diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 6fd18280fe..b80642ee43 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-16 Basis Technology Corp. + * Copyright 2011-17 Basis Technology Corp. * * Copyright 2012 42six Solutions. * Contact: aebadirad 42six com @@ -40,6 +40,7 @@ import java.util.List; import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -93,7 +94,16 @@ public class ImageUtils { private static final List SUPPORTED_IMAGE_EXTENSIONS = new ArrayList<>(); private static final SortedSet SUPPORTED_IMAGE_MIME_TYPES; - private static final boolean openCVLoaded; + private static final boolean OPEN_CV_LOADED; + + /** + * Map from tsk object id to Java File object. Used to get the same File for + * different tasks related to the same object so we can then synchronize on + * the File. + * + * NOTE: Must be cleared when the case is changed. + */ + private static final ConcurrentHashMap cacheFileMap = new ConcurrentHashMap<>(); static { ImageIO.scanForPlugins(); @@ -124,7 +134,7 @@ public class ImageUtils { } - openCVLoaded = openCVLoadedTemp; + OPEN_CV_LOADED = 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())); @@ -139,6 +149,9 @@ public class ImageUtils { "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 + + //Clear the file map when the case changes, so we don't accidentaly get images from the old case. + Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), evt -> cacheFileMap.clear()); } /** @@ -147,7 +160,7 @@ public class ImageUtils { private static FileTypeDetector fileTypeDetector; /** - * thread that saves generated thumbnails to disk in the background + *Thread/Executor that saves generated thumbnails to disk in the background */ private static final Executor imageSaver = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() @@ -296,7 +309,7 @@ public class ImageUtils { 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 + LOGGER.log(Level.WARNING, "Failed to get thumbnail for " + getContentPathSafe(content), ex); //NON-NLS return DEFAULT_THUMBNAIL; } } else { @@ -305,7 +318,6 @@ public class ImageUtils { } /** - * * Get a thumbnail of a specified size for the given image. Generates the * thumbnail if it is not already cached. * @@ -332,13 +344,15 @@ public class ImageUtils { * 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; - } + return cacheFileMap.computeIfAbsent(fileID, id -> { + 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; + } + }); } /** @@ -489,6 +503,7 @@ public class ImageUtils { "ImageIO could not determine height of {0}: ", //NON-NLS imageReader -> imageReader.getHeight(0) ); + } /** @@ -576,6 +591,7 @@ public class ImageUtils { */ public static Task newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { return new GetThumbnailTask(file, iconSize, defaultOnFailure); + } /** @@ -609,16 +625,21 @@ public class ImageUtils { 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); + if (cacheFile != null) { + synchronized (cacheFile) { + if (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. + } } - } 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. } } @@ -629,7 +650,7 @@ public class ImageUtils { //There was no correctly-sized cached thumbnail so make one. BufferedImage thumbnail = null; if (VideoUtils.isVideoThumbnailSupported(file)) { - if (openCVLoaded) { + if (OPEN_CV_LOADED) { updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); } @@ -698,12 +719,14 @@ public class ImageUtils { private void saveThumbnail(BufferedImage thumbnail) { imageSaver.execute(() -> { try { - Files.createParentDirs(cacheFile); - if (cacheFile.exists()) { - cacheFile.delete(); + synchronized (cacheFile) { + Files.createParentDirs(cacheFile); + if (cacheFile.exists()) { + cacheFile.delete(); + } + ImageIO.write(thumbnail, FORMAT, cacheFile); } - ImageIO.write(thumbnail, FORMAT, cacheFile); - } catch (IllegalArgumentException | IOException ex) { + } catch (Exception ex) { LOGGER.log(Level.WARNING, "Could not write thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS } }); @@ -725,6 +748,7 @@ public class ImageUtils { */ public static Task newReadImageTask(AbstractFile file) { return new ReadImageTask(file); + } /** @@ -753,7 +777,6 @@ public class ImageUtils { 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;