org.sleuthkit.autopsy.coreutils.ImageUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.coreutils.ImageUtils.java

Source

/*
 * 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);

    }

}