/* * Autopsy Forensic Browser * * Copyright 2012-16 Basis Technology Corp. * * Copyright 2012 42six Solutions. * Contact: aebadirad <at> 42six <dot> com * Project Contact/Architect: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.coreutils; import com.google.common.collect.ImmutableSortedSet; import com.google.common.io.Files; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.logging.Level; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.event.IIOReadProgressListener; import javax.imageio.stream.ImageInputStream; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.opencv.core.Core; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector.FileTypeDetectorInitException; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskCoreException; /** * Utilities for working with image files and creating thumbnails. Re-uses * thumbnails by storing them in the case's cache directory. */ public class ImageUtils { private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); /** * save thumbnails to disk as this format */ private static final String FORMAT = "png"; //NON-NLS public static final int ICON_SIZE_SMALL = 50; public static final int ICON_SIZE_MEDIUM = 100; public static final int ICON_SIZE_LARGE = 200; private static final BufferedImage DEFAULT_THUMBNAIL; private static final List<String> GIF_EXTENSION_LIST = Arrays.asList("gif"); private static final SortedSet<String> GIF_MIME_SET = ImmutableSortedSet.copyOf(new String[]{"image/gif"}); private static final List<String> SUPPORTED_IMAGE_EXTENSIONS = new ArrayList<>(); private static final SortedSet<String> SUPPORTED_IMAGE_MIME_TYPES; private static final boolean openCVLoaded; static { ImageIO.scanForPlugins(); BufferedImage tempImage; try { tempImage = ImageIO.read(ImageUtils.class.getResourceAsStream("/org/sleuthkit/autopsy/images/file-icon.png"));//NON-NLS } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Failed to load default icon.", ex); //NON-NLS tempImage = null; } DEFAULT_THUMBNAIL = tempImage; //load opencv libraries boolean openCVLoadedTemp; try { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { //NON-NLS System.loadLibrary("opencv_ffmpeg248_64"); //NON-NLS } else { System.loadLibrary("opencv_ffmpeg248"); //NON-NLS } openCVLoadedTemp = true; } catch (UnsatisfiedLinkError e) { openCVLoadedTemp = false; LOGGER.log(Level.SEVERE, "OpenCV Native code library failed to load", e); //NON-NLS //TODO: show warning bubble } openCVLoaded = openCVLoadedTemp; SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes())); SUPPORTED_IMAGE_EXTENSIONS.add("tec"); // Add JFIF .tec files SUPPORTED_IMAGE_MIME_TYPES = new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes())); /* * special cases and variants that we support, but don't get registered * with ImageIO automatically */ SUPPORTED_IMAGE_MIME_TYPES.addAll(Arrays.asList( "image/x-rgb", //NON-NLS "image/x-ms-bmp", //NON-NLS "image/x-portable-graymap", //NON-NLS "image/x-portable-bitmap", //NON-NLS "application/x-123")); //TODO: is this correct? -jm //NON-NLS SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); //NON-NLS } /** * initialized lazily */ private static FileTypeDetector fileTypeDetector; /** * thread that saves generated thumbnails to disk in the background */ private static final Executor imageSaver = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() .namingPattern("thumbnail-saver-%d").build()); //NON-NLS public static List<String> getSupportedImageExtensions() { return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS); } public static SortedSet<String> getSupportedImageMimeTypes() { return Collections.unmodifiableSortedSet(SUPPORTED_IMAGE_MIME_TYPES); } /** * Get the default thumbnail, which is the icon for a file. Used when we can * not generate a content based thumbnail. * * @return the default thumbnail */ public static Image getDefaultThumbnail() { return DEFAULT_THUMBNAIL; } /** * Can a thumbnail be generated for the content? * * Although this method accepts Content, it always returns false for objects * that are not instances of AbstractFile. * * @param content A content object to test for thumbnail support. * * @return true if a thumbnail can be generated for the given content. */ public static boolean thumbnailSupported(Content content) { if (!(content instanceof AbstractFile)) { return false; } AbstractFile file = (AbstractFile) content; return VideoUtils.isVideoThumbnailSupported(file) || isImageThumbnailSupported(file); } /** * Is the file an image that we can read and generate a thumbnail for? * * @param file the AbstractFile to test * * @return true if the file is an image we can read and generate thumbnail * for. */ public static boolean isImageThumbnailSupported(AbstractFile file) { return isMediaThumbnailSupported(file, "image/", SUPPORTED_IMAGE_MIME_TYPES, SUPPORTED_IMAGE_EXTENSIONS) || hasImageFileHeader(file);//NON-NLS } /** * Checks the MIME type and/or extension of a file to determine whether it * is a GIF. * * @param file the AbstractFile to test * * @return true if the file is a gif */ public static boolean isGIF(AbstractFile file) { return isMediaThumbnailSupported(file, null, GIF_MIME_SET, GIF_EXTENSION_LIST); } /** * Check if making a thumbnail for the given file is supported by checking * its extension and/or MIME type against the supplied collections. * * //TODO: this should move to a better place. Should ImageUtils and * VideoUtils both implement/extend some base interface/abstract class. That * would be the natural place to put this. * * @param file the AbstractFile to test * @param mimeTypePrefix a MIME 'top-level type name' such as "image/", * including the "/". In addition to the list of * supported MIME types, any type that starts with * this prefix will be regarded as supported * @param supportedMimeTypes a collection of mimetypes that are supported * @param supportedExtension a collection of extensions that are supported * * @return true if a thumbnail can be generated for the given file based on * the given MIME type prefix and lists of supported MIME types and * extensions */ static boolean isMediaThumbnailSupported(AbstractFile file, String mimeTypePrefix, final Collection<String> supportedMimeTypes, final List<String> supportedExtension) { if (false == file.isFile() || file.getSize() <= 0) { return false; } String extension = file.getNameExtension(); if (StringUtils.isNotBlank(extension) && supportedExtension.contains(extension)) { return true; } else { try { String mimeType = getFileTypeDetector().detect(file); if (StringUtils.isNotBlank(mimeTypePrefix) && mimeType.startsWith(mimeTypePrefix)) { return true; } return supportedMimeTypes.contains(mimeType); } catch (FileTypeDetectorInitException | TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error determining MIME type of " + getContentPathSafe(file), ex);//NON-NLS return false; } } } /** * //TODO: AUT-2057 this FileTypeDetector needs to be recreated when the * user adds new user defined file types. * * get a FileTypeDetector * * @return a FileTypeDetector * * @throws FileTypeDetectorInitException if initializing the * FileTypeDetector failed. */ synchronized private static FileTypeDetector getFileTypeDetector() throws FileTypeDetector.FileTypeDetectorInitException { if (fileTypeDetector == null) { fileTypeDetector = new FileTypeDetector(); } return fileTypeDetector; } /** * Get a thumbnail of a specified size for the given image. Generates the * thumbnail if it is not already cached. * * @param content the content to generate a thumbnail for * @param iconSize the size (one side of a square) in pixels to generate * * @return a thumbnail for the given image or a default one if there was a * problem making a thumbnail. */ public static BufferedImage getThumbnail(Content content, int iconSize) { if (content instanceof AbstractFile) { AbstractFile file = (AbstractFile) content; Task<javafx.scene.image.Image> thumbnailTask = newGetThumbnailTask(file, iconSize, true); thumbnailTask.run(); try { return SwingFXUtils.fromFXImage(thumbnailTask.get(), null); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.WARNING, "Failed to get thumbnail for {0}: " + ex.toString(), getContentPathSafe(content)); //NON-NLS return DEFAULT_THUMBNAIL; } } else { return DEFAULT_THUMBNAIL; } } /** * * Get a thumbnail of a specified size for the given image. Generates the * thumbnail if it is not already cached. * * @param content the content to generate a thumbnail for * @param iconSize the size (one side of a square) in pixels to generate * * @return File object for cached image. Is guaranteed to exist, as long as * there was not an error generating or saving the thumbnail. */ @Nullable public static File getCachedThumbnailFile(Content content, int iconSize) { getThumbnail(content, iconSize); return getCachedThumbnailLocation(content.getId()); } /** * Get the location of the cached thumbnail for a file with the given fileID * as a java File. The returned File may not exist on disk yet. * * @param fileID the fileID to get the cached thumbnail location for * * @return a File object representing the location of the cached thumbnail. * This file may not actually exist(yet). Returns null if there was * any problem getting the file, such as no case was open. */ private static File getCachedThumbnailLocation(long fileID) { try { String cacheDirectory = Case.getCurrentCase().getCacheDirectory(); return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS } catch (IllegalStateException e) { LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS return null; } } /** * Do a direct check to see if the given file has an image file header. * NOTE: Currently only jpeg and png are supported. * * @param file the AbstractFile to check * * @return true if the given file has one of the supported image headers. */ public static boolean hasImageFileHeader(AbstractFile file) { return isJpegFileHeader(file) || isPngFileHeader(file); } /** * Check if the given file is a jpeg based on header. * * @param file the AbstractFile to check * * @return true if jpeg file, false otherwise */ public static boolean isJpegFileHeader(AbstractFile file) { if (file.getSize() < 100) { return false; } try { byte[] fileHeaderBuffer = readHeader(file, 2); /* * Check for the JPEG header. Since Java bytes are signed, we cast * them to an int first. */ return (((fileHeaderBuffer[0] & 0xff) == 0xff) && ((fileHeaderBuffer[1] & 0xff) == 0xd8)); } catch (TskCoreException ex) { //ignore if can't read the first few bytes, not a JPEG return false; } } /** * Find the offset for the first Start Of Image marker (0xFFD8) in JFIF, * allowing for leading End Of Image markers. * * @param file the AbstractFile to parse * * @return Offset of first Start Of Image marker, or 0 if none found. This * will let ImageIO try to open it from offset 0. */ private static long getJfifStartOfImageOffset(AbstractFile file) { byte[] fileHeaderBuffer; long length; try { length = file.getSize(); if (length % 2 != 0) { length -= 1; // Make it an even number so we can parse two bytes at a time } if (length >= 1024) { length = 1024; } fileHeaderBuffer = readHeader(file, (int) length); // read up to first 1024 bytes } catch (TskCoreException ex) { // Couldn't read header. Let ImageIO try it. return 0; } if (fileHeaderBuffer != null) { for (int index = 0; index < length; index += 2) { // Look for Start Of Image marker and return the index when it's found if ((fileHeaderBuffer[index] == (byte) 0xFF) && (fileHeaderBuffer[index + 1] == (byte) 0xD8)) { return index; } } } // Didn't match JFIF. Let ImageIO try to open it from offset 0. return 0; } /** * Check if the given file is a png based on header. * * @param file the AbstractFile to check * * @return true if png file, false otherwise */ public static boolean isPngFileHeader(AbstractFile file) { if (file.getSize() < 10) { return false; } try { byte[] fileHeaderBuffer = readHeader(file, 8); /* * Check for the png header. Since Java bytes are signed, we cast * them to an int first. */ return (((fileHeaderBuffer[1] & 0xff) == 0x50) && ((fileHeaderBuffer[2] & 0xff) == 0x4E) && ((fileHeaderBuffer[3] & 0xff) == 0x47) && ((fileHeaderBuffer[4] & 0xff) == 0x0D) && ((fileHeaderBuffer[5] & 0xff) == 0x0A) && ((fileHeaderBuffer[6] & 0xff) == 0x1A) && ((fileHeaderBuffer[7] & 0xff) == 0x0A)); } catch (TskCoreException ex) { //ignore if can't read the first few bytes, not an png return false; } } private static byte[] readHeader(AbstractFile file, int buffLength) throws TskCoreException { byte[] fileHeaderBuffer = new byte[buffLength]; int bytesRead = file.read(fileHeaderBuffer, 0, buffLength); if (bytesRead != buffLength) { //ignore if can't read the first few bytes, not an image throw new TskCoreException("Could not read " + buffLength + " bytes from " + file.getName());//NON-NLS } return fileHeaderBuffer; } /** * Get the width of the given image, in pixels. * * @param file * * @return the width in pixels * * @throws IOException If the file is not a supported image or the width * could not be determined. */ static public int getImageWidth(AbstractFile file) throws IOException { return getImageProperty(file, "ImageIO could not determine width of {0}: ", //NON-NLS imageReader -> imageReader.getWidth(0) ); } /** * Get the height of the given image,in pixels. * * @param file * * @return the height in pixels * * @throws IOException If the file is not a supported image or the height * could not be determined. */ static public int getImageHeight(AbstractFile file) throws IOException { return getImageProperty(file, "ImageIO could not determine height of {0}: ", //NON-NLS imageReader -> imageReader.getHeight(0) ); } /** * Functional interface for methods that extract a property out of an * ImageReader. Initially created to abstract over * {@link #getImageHeight(org.sleuthkit.datamodel.AbstractFile)} and * {@link #getImageWidth(org.sleuthkit.datamodel.AbstractFile)} * * @param <T> The type of the property. */ @FunctionalInterface private static interface PropertyExtractor<T> { public T extract(ImageReader reader) throws IOException; } /** * Private template method designed to be used as the implementation of * public methods that pull particular (usually meta-)data out of a image * file. * * @param file the file to extract the data from * @param errorTemplate a message template used to log errors. Should * take one parameter: the file's unique path or * name. * @param propertyExtractor an implementation of {@link PropertyExtractor} * used to retrieve the specific property. * * @return the the value of the property extracted by the given * propertyExtractor * * @throws IOException if there was a problem reading the property from the * file. * * @see PropertyExtractor * @see #getImageHeight(org.sleuthkit.datamodel.AbstractFile) */ private static <T> T getImageProperty(AbstractFile file, final String errorTemplate, PropertyExtractor<T> propertyExtractor) throws IOException { try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) { if (input == null) { IIOException iioException = new IIOException("Could not create ImageInputStream."); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file)); throw iioException; } Iterator<ImageReader> readers = ImageIO.getImageReaders(input); if (readers.hasNext()) { ImageReader reader = readers.next(); reader.setInput(input); try { return propertyExtractor.extract(reader); } catch (IOException ex) { LOGGER.log(Level.WARNING, errorTemplate + ex.toString(), getContentPathSafe(file)); throw ex; } finally { reader.dispose(); } } else { IIOException iioException = new IIOException("No ImageReader found."); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file)); throw iioException; } } } } /** * Create a new Task that will get a thumbnail for the given image of the * specified size. If a cached thumbnail is available it will be returned as * the result of the task, otherwise a new thumbnail will be created and * cached. * * Note: the returned task is suitable for running in a background thread, * but is not started automatically. Clients are responsible for running the * task, monitoring its progress, and using its result. * * @param file The file to create a thumbnail for. * @param iconSize The size of the thumbnail. * @param defaultOnFailure Whether or not to default on failure. * * @return a new Task that returns a thumbnail as its result. */ public static Task<javafx.scene.image.Image> newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { return new GetThumbnailTask(file, iconSize, defaultOnFailure); } /** * A Task that gets cached thumbnails and makes new ones as needed. */ static private class GetThumbnailTask extends ReadImageTaskBase { private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read {0} for thumbnail generation."; //NON-NLS private final int iconSize; private final File cacheFile; private final boolean defaultOnFailure; @NbBundle.Messages({"# {0} - file name", "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", "# {0} - file name", "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"}) private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { super(file); updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName())); this.iconSize = iconSize; this.defaultOnFailure = defaultOnFailure; this.cacheFile = getCachedThumbnailLocation(file.getId()); } @Override protected javafx.scene.image.Image call() throws Exception { if (isGIF(file)) { return readImage(); } if (isCancelled()) { return null; } // If a thumbnail file is already saved locally, just read that. if (cacheFile != null && cacheFile.exists()) { try { BufferedImage cachedThumbnail = ImageIO.read(cacheFile); if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { return SwingFXUtils.toFXImage(cachedThumbnail, null); } } catch (Exception ex) { LOGGER.log(Level.WARNING, "ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS cacheFile.delete(); //since we can't read the file we might as well delete it. } } if (isCancelled()) { return null; } //There was no correctly-sized cached thumbnail so make one. BufferedImage thumbnail = null; if (VideoUtils.isVideoThumbnailSupported(file)) { if (openCVLoaded) { updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); } if (null == thumbnail) { if (defaultOnFailure) { thumbnail = DEFAULT_THUMBNAIL; } else { throw new IIOException("Failed to generate a thumbnail for " + getContentPathSafe(file));//NON-NLS } } } else { //read the image into a buffered image. //TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null); if (null == bufferedImage) { String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file)); LOGGER.log(Level.WARNING, msg); throw new IIOException(msg); } updateProgress(-1, 1); //resize, or if that fails, crop it try { thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); } catch (IllegalArgumentException | OutOfMemoryError e) { // if resizing does not work due to extreme aspect ratio or oom, crop the image instead. LOGGER.log(Level.WARNING, "Cropping {0}, because it could not be scaled: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS final int height = bufferedImage.getHeight(); final int width = bufferedImage.getWidth(); if (iconSize < height || iconSize < width) { final int cropHeight = Math.min(iconSize, height); final int cropWidth = Math.min(iconSize, width); try { thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight); } catch (Exception cropException) { LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS } } } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not scale {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS throw e; } } if (isCancelled()) { return null; } updateProgress(-1, 1); //if we got a valid thumbnail save it if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) { saveThumbnail(thumbnail); } return SwingFXUtils.toFXImage(thumbnail, null); } /** * submit the thumbnail saving to another background thread. * * @param thumbnail */ private void saveThumbnail(BufferedImage thumbnail) { imageSaver.execute(() -> { try { Files.createParentDirs(cacheFile); if (cacheFile.exists()) { cacheFile.delete(); } ImageIO.write(thumbnail, FORMAT, cacheFile); } catch (IllegalArgumentException | IOException ex) { LOGGER.log(Level.WARNING, "Could not write thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS } }); } } /** * Create a new Task that will read the file into memory as an * javafx.scene.image.Image. * * Note: the returned task is suitable for running in a background thread, * but is not started automatically. Clients are responsible for running the * task, monitoring its progress, and using its result(including testing for * null). * * @param file the file to read as an Image * * @return a new Task that returns an Image as its result */ public static Task<javafx.scene.image.Image> newReadImageTask(AbstractFile file) { return new ReadImageTask(file); } /** * A task that reads the content of a AbstractFile as a javafx Image. */ @NbBundle.Messages({ "# {0} - file name", "ReadImageTask.mesageText=Reading image: {0}"}) static private class ReadImageTask extends ReadImageTaskBase { ReadImageTask(AbstractFile file) { super(file); updateMessage(Bundle.ReadImageTask_mesageText(file.getName())); } @Override protected javafx.scene.image.Image call() throws Exception { return readImage(); } } /** * Base class for tasks that need to read AbstractFiles as Images. */ static private abstract class ReadImageTaskBase extends Task<javafx.scene.image.Image> implements IIOReadProgressListener { private static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NON-NLS final AbstractFile file; // private ImageReader reader; ReadImageTaskBase(AbstractFile file) { this.file = file; } protected javafx.scene.image.Image readImage() throws IOException { if (ImageUtils.isGIF(file)) { //use JavaFX to directly read GIF to preserve potential animation javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(new ReadContentInputStream(file))); if (image.isError() == false) { return image; } } else if (file.getNameExtension().equalsIgnoreCase("tec")) { //NON-NLS ReadContentInputStream readContentInputStream = new ReadContentInputStream(file); // Find first Start Of Image marker readContentInputStream.seek(getJfifStartOfImageOffset(file)); //use JavaFX to directly read .tec files javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(readContentInputStream)); if (image.isError() == false) { return image; } } //fall through to default image reading code if there was an error if (isCancelled()) { return null; } return getImageProperty(file, "ImageIO could not read {0}: ", imageReader -> { imageReader.addIIOReadProgressListener(ReadImageTaskBase.this); /* * This is the important part, get or create a * ImageReadParam, create a destination image to hold * the decoded result, then pass that image with the * param. */ ImageReadParam param = imageReader.getDefaultReadParam(); BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0)); param.setDestination(bufferedImage); try { bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object } catch (IOException iOException) { LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS } finally { imageReader.removeIIOReadProgressListener(ReadImageTaskBase.this); } if (isCancelled()) { return null; } return SwingFXUtils.toFXImage(bufferedImage, null); } ); } @Override public void imageProgress(ImageReader reader, float percentageDone) { //update this task with the progress reported by ImageReader.read updateProgress(percentageDone, 100); if (isCancelled()) { reader.removeIIOReadProgressListener(this); reader.abort(); reader.dispose(); } } @Override protected void succeeded() { super.succeeded(); try { javafx.scene.image.Image fxImage = get(); if (fxImage == null) { LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT, ImageUtils.getContentPathSafe(file)); } else if (fxImage.isError()) { //if there was somekind of error, log it LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file)); } } catch (InterruptedException | ExecutionException ex) { failed(); } } @Override protected void failed() { super.failed(); LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file)); } @Override public void imageComplete(ImageReader source) { updateProgress(100, 100); } @Override public void imageStarted(ImageReader source, int imageIndex) { } @Override public void sequenceStarted(ImageReader source, int minIndex) { } @Override public void sequenceComplete(ImageReader source) { } @Override public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { } @Override public void thumbnailProgress(ImageReader source, float percentageDone) { } @Override public void thumbnailComplete(ImageReader source) { } @Override public void readAborted(ImageReader source) { } } /** * Get the unique path for the content, or if that fails, just return the * name. * * @param content * * @return the unique path for the content, or if that fails, just the name. */ static String getContentPathSafe(Content content) { try { return content.getUniquePath(); } catch (TskCoreException tskCoreException) { String contentName = content.getName(); LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NON-NLS return contentName; } } /** * Get the default thumbnail, which is the icon for a file. Used when we can * not generate content based thumbnail. * * @return * * @deprecated use {@link #getDefaultThumbnail() } instead. */ @Deprecated public static Image getDefaultIcon() { return getDefaultThumbnail(); } /** * Get a file object for where the cached icon should exist. The returned * file may not exist. * * @param id * * @return * * @deprecated use {@link #getCachedThumbnailLocation(long) } instead */ @Deprecated public static File getFile(long id) { return getCachedThumbnailLocation(id); } /** * Get a thumbnail of a specified size for the given image. Generates the * thumbnail if it is not already cached. * * @param content * @param iconSize * * @return a thumbnail for the given image or a default one if there was a * problem making a thumbnail. * * @deprecated use {@link #getThumbnail(org.sleuthkit.datamodel.Content, int) * } instead. */ @Nonnull @Deprecated public static BufferedImage getIcon(Content content, int iconSize) { return getThumbnail(content, iconSize); } /** * Get a thumbnail of a specified size for the given image. Generates the * thumbnail if it is not already cached. * * @param content * @param iconSize * * @return File object for cached image. Is guaranteed to exist, as long as * there was not an error generating or saving the thumbnail. * * @deprecated use {@link #getCachedThumbnailFile(org.sleuthkit.datamodel.Content, int) * } instead. * */ @Nullable @Deprecated public static File getIconFile(Content content, int iconSize) { return getCachedThumbnailFile(content, iconSize); } }