org.tinymediamanager.core.ImageCache.java Source code

Java tutorial

Introduction

Here is the source code for org.tinymediamanager.core.ImageCache.java

Source

/*
 * Copyright 2012 - 2016 Manuel Laggner
 *
 * 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.tinymediamanager.core;

import java.awt.Image;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.imgscalr.Scalr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tinymediamanager.Globals;
import org.tinymediamanager.core.entities.MediaEntity;
import org.tinymediamanager.core.entities.MediaFile;
import org.tinymediamanager.scraper.http.Url;
import org.tinymediamanager.scraper.util.UrlUtil;
import org.tinymediamanager.thirdparty.ImageLoader;

/**
 * The Class ImageCache - used to build a local image cache (scaled down versions & thumbnails - also for offline access).
 * 
 * @author Manuel Laggner
 */
public class ImageCache {
    private static final Logger LOGGER = LoggerFactory.getLogger(ImageCache.class);
    private static final Path CACHE_DIR = Paths.get("cache/image");

    public enum CacheType {
        FAST, SMOOTH
    }

    /**
     * Gets the cache dir. If it is not on the disk - it will also create it
     * 
     * @return the cache dir
     */
    public static Path getCacheDir() {
        if (!Files.exists(CACHE_DIR)) {
            try {
                Files.createDirectories(CACHE_DIR);
            } catch (IOException e) {
                LOGGER.warn("Could not create cache dir " + CACHE_DIR + " - " + e.getMessage());
            }
        }
        return CACHE_DIR;
    }

    /**
     * Gets the file name (MD5 hash) of the cached file.
     * 
     * @param path
     *          the url
     * @return the cached file name
     */
    public static String getMD5(String path) {
        try {
            if (path == null) {
                return null;
            }
            // now uses a simple md5 hash, which should have a fairly low collision
            // rate, especially for our limited use
            byte[] key = DigestUtils.md5(path);
            return new String(Hex.encodeHex(key));
        } catch (Exception e) {
            LOGGER.error("Failed to create cached filename for image: " + path, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Scale image to fit in the given width.
     * 
     * @param imageUrl
     *          the image url
     * @param width
     *          the width
     * @return the input stream
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     * @throws InterruptedException
     */
    public static InputStream scaleImage(String imageUrl, int width) throws IOException, InterruptedException {
        Url url = new Url(imageUrl);

        BufferedImage originalImage = null;
        try {
            originalImage = createImage(url.getBytes());
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }

        Point size = new Point();
        size.x = width;
        size.y = size.x * originalImage.getHeight() / originalImage.getWidth();

        // BufferedImage scaledImage = Scaling.scale(originalImage, size.x, size.y);
        BufferedImage scaledImage = Scalr.resize(originalImage, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC, size.x,
                size.y, Scalr.OP_ANTIALIAS);
        originalImage = null;

        ImageWriter imgWrtr = null;
        ImageWriteParam imgWrtrPrm = null;

        // here we have two different ways to create our thumb
        // a) a scaled down jpg/png (without transparency) which we have to modify since OpenJDK cannot call native jpg encoders
        // b) a scaled down png (with transparency) which we can store without any more modifying as png
        if (hasTransparentPixels(scaledImage)) {
            // transparent image -> png
            imgWrtr = ImageIO.getImageWritersByFormatName("png").next();
            imgWrtrPrm = imgWrtr.getDefaultWriteParam();

        } else {
            // non transparent image -> jpg
            // convert to rgb
            BufferedImage rgb = new BufferedImage(scaledImage.getWidth(), scaledImage.getHeight(),
                    BufferedImage.TYPE_INT_RGB);
            ColorConvertOp xformOp = new ColorConvertOp(null);
            xformOp.filter(scaledImage, rgb);
            imgWrtr = ImageIO.getImageWritersByFormatName("jpg").next();
            imgWrtrPrm = imgWrtr.getDefaultWriteParam();
            imgWrtrPrm.setCompressionMode(JPEGImageWriteParam.MODE_EXPLICIT);
            imgWrtrPrm.setCompressionQuality(0.80f);

            scaledImage = rgb;
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageOutputStream output = ImageIO.createImageOutputStream(baos);
        imgWrtr.setOutput(output);
        IIOImage outputImage = new IIOImage(scaledImage, null, null);
        imgWrtr.write(null, outputImage, imgWrtrPrm);
        imgWrtr.dispose();
        scaledImage = null;

        byte[] bytes = baos.toByteArray();

        output.flush();
        output.close();
        baos.close();

        return new ByteArrayInputStream(bytes);
    }

    /**
     * Scale image to fit in the given width.
     * 
     * @param file
     *          the original image file
     * @param width
     *          the width
     * @return the input stream
     * @throws IOException
     *           Signals that an I/O exception has occurred.
     * @throws InterruptedException
     */
    public static InputStream scaleImage(Path file, int width) throws IOException, InterruptedException {
        BufferedImage originalImage = null;
        try {
            originalImage = createImage(file);
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }

        Point size = new Point();
        size.x = width;
        size.y = size.x * originalImage.getHeight() / originalImage.getWidth();

        // BufferedImage scaledImage = Scaling.scale(originalImage, size.x, size.y);
        BufferedImage scaledImage = Scalr.resize(originalImage, Scalr.Method.QUALITY, Scalr.Mode.AUTOMATIC, size.x,
                size.y, Scalr.OP_ANTIALIAS);
        originalImage = null;

        ImageWriter imgWrtr = null;
        ImageWriteParam imgWrtrPrm = null;

        // here we have two different ways to create our thumb
        // a) a scaled down jpg/png (without transparency) which we have to modify since OpenJDK cannot call native jpg encoders
        // b) a scaled down png (with transparency) which we can store without any more modifying as png
        if (hasTransparentPixels(scaledImage)) {
            // transparent image -> png
            imgWrtr = ImageIO.getImageWritersByFormatName("png").next();
            imgWrtrPrm = imgWrtr.getDefaultWriteParam();

        } else {
            // non transparent image -> jpg
            // convert to rgb
            BufferedImage rgb = new BufferedImage(scaledImage.getWidth(), scaledImage.getHeight(),
                    BufferedImage.TYPE_INT_RGB);
            ColorConvertOp xformOp = new ColorConvertOp(null);
            xformOp.filter(scaledImage, rgb);
            imgWrtr = ImageIO.getImageWritersByFormatName("jpg").next();
            imgWrtrPrm = imgWrtr.getDefaultWriteParam();
            imgWrtrPrm.setCompressionMode(JPEGImageWriteParam.MODE_EXPLICIT);
            imgWrtrPrm.setCompressionQuality(0.80f);

            scaledImage = rgb;
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageOutputStream output = ImageIO.createImageOutputStream(baos);
        imgWrtr.setOutput(output);
        IIOImage outputImage = new IIOImage(scaledImage, null, null);
        imgWrtr.write(null, outputImage, imgWrtrPrm);
        imgWrtr.dispose();
        scaledImage = null;

        byte[] bytes = baos.toByteArray();

        output.flush();
        output.close();
        baos.close();

        return new ByteArrayInputStream(bytes);
    }

    /**
     * Cache image.
     * 
     * @param mf
     *          the media file
     * @return the file the cached file
     * @throws Exception
     */
    public static Path cacheImage(Path originalFile) throws Exception {
        MediaFile mf = new MediaFile(originalFile);
        Path cachedFile = ImageCache.getCacheDir()
                .resolve(getMD5(originalFile.toString()) + "." + Utils.getExtension(originalFile));
        if (!Files.exists(cachedFile)) {
            // check if the original file exists && size > 0
            if (!Files.exists(originalFile)) {
                throw new FileNotFoundException("unable to cache file: " + originalFile + "; file does not exist");
            }
            if (Files.size(originalFile) == 0) {
                throw new EmptyFileException(originalFile);
            }

            // recreate cache dir if needed
            // rescale & cache
            BufferedImage originalImage = null;
            try {
                originalImage = createImage(originalFile);
            } catch (Exception e) {
                throw new Exception("cannot create image - file seems not to be valid? " + originalFile);
            }

            // calculate width based on MF type
            int desiredWidth = originalImage.getWidth(); // initialize with fallback
            switch (mf.getType()) {
            case FANART:
                if (originalImage.getWidth() > 1000) {
                    desiredWidth = 1000;
                }
                break;

            case POSTER:
                if (originalImage.getHeight() > 500) {
                    desiredWidth = 350;
                }
                break;

            case EXTRAFANART:
            case THUMB:
            case BANNER:
            case GRAPHIC:
                desiredWidth = 300;
                break;

            default:
                break;
            }

            // special handling for movieset-fanart or movieset-poster
            if (mf.getFilename().startsWith("movieset-fanart") || mf.getFilename().startsWith("movieset-poster")) {
                if (originalImage.getWidth() > 1000) {
                    desiredWidth = 1000;
                }
            }

            Point size = calculateSize(desiredWidth, (int) (originalImage.getHeight() / 1.5),
                    originalImage.getWidth(), originalImage.getHeight(), true);
            BufferedImage scaledImage = null;

            if (Globals.settings.getImageCacheType() == CacheType.FAST) {
                // scale fast
                scaledImage = Scalr.resize(originalImage, Scalr.Method.BALANCED, Scalr.Mode.FIT_EXACT, size.x,
                        size.y);
            } else {
                // scale with good quality
                scaledImage = Scalr.resize(originalImage, Scalr.Method.QUALITY, Scalr.Mode.FIT_EXACT, size.x,
                        size.y);
            }
            originalImage = null;

            ImageWriter imgWrtr = null;
            ImageWriteParam imgWrtrPrm = null;

            // here we have two different ways to create our thumb
            // a) a scaled down jpg/png (without transparency) which we have to modify since OpenJDK cannot call native jpg encoders
            // b) a scaled down png (with transparency) which we can store without any more modifying as png
            if (hasTransparentPixels(scaledImage)) {
                // transparent image -> png
                imgWrtr = ImageIO.getImageWritersByFormatName("png").next();
                imgWrtrPrm = imgWrtr.getDefaultWriteParam();

            } else {
                // non transparent image -> jpg
                // convert to rgb
                BufferedImage rgb = new BufferedImage(scaledImage.getWidth(), scaledImage.getHeight(),
                        BufferedImage.TYPE_INT_RGB);
                ColorConvertOp xformOp = new ColorConvertOp(null);
                xformOp.filter(scaledImage, rgb);
                imgWrtr = ImageIO.getImageWritersByFormatName("jpg").next();
                imgWrtrPrm = imgWrtr.getDefaultWriteParam();
                imgWrtrPrm.setCompressionMode(JPEGImageWriteParam.MODE_EXPLICIT);
                imgWrtrPrm.setCompressionQuality(0.80f);

                scaledImage = rgb;
            }

            FileImageOutputStream output = new FileImageOutputStream(cachedFile.toFile());
            imgWrtr.setOutput(output);
            IIOImage image = new IIOImage(scaledImage, null, null);
            imgWrtr.write(null, image, imgWrtrPrm);
            imgWrtr.dispose();
            output.flush();
            output.close();
            scaledImage = null;
        }

        if (!Files.exists(cachedFile)) {
            throw new Exception("unable to cache file: " + originalFile);
        }

        return cachedFile;
    }

    private static boolean hasTransparentPixels(BufferedImage image) {
        for (int x = 0; x < image.getWidth(); x++) {
            for (int y = 0; y < image.getHeight(); y++) {
                int pixel = image.getRGB(x, y);
                if ((pixel >> 24) == 0x00) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Invalidate cached image.
     * 
     * @param path
     *          the path
     */
    public static void invalidateCachedImage(Path path) {
        Path cachedFile = getCacheDir()
                .resolve(ImageCache.getMD5(path.toAbsolutePath().toString()) + "." + Utils.getExtension(path));
        if (Files.exists(cachedFile)) {
            Utils.deleteFileSafely(cachedFile);
        }
    }

    /**
     * Gets the cached image for "string".<br>
     * If not found AND it is a valid url, download and cache first.<br>
     * 
     * @param url
     *          the url of image, or basically the unhashed string of cache file
     * @return the cached file or NULL
     */
    public static Path getCachedFile(String url) {
        if (url == null || url.isEmpty()) {
            return null;
        }

        String ext = UrlUtil.getExtension(url);
        if (ext.isEmpty()) {
            ext = "jpg"; // just assume
        }
        Path cachedFile = ImageCache.getCacheDir().resolve(getMD5(url) + "." + ext);
        if (Files.exists(cachedFile)) {
            LOGGER.trace("found cached url :) " + url);
            return cachedFile;
        }

        // is the image cache activated?
        if (!Globals.settings.isImageCache()) {
            return null;
        }

        try {
            Url u = new Url(url);
            boolean ok = u.download(cachedFile);
            if (ok) {
                LOGGER.trace("cached url successfully :) " + url);
                return cachedFile;
            }
        } catch (MalformedURLException e) {
            LOGGER.trace("Problem getting cached file for url " + e.getMessage());
        }

        LOGGER.trace("Problem getting cached file for url " + url);
        return null;
    }

    /**
     * Gets the cached file, if ImageCache is activated<br>
     * If not found, cache original first
     * 
     * @param path
     *          the path
     * @return the cached file
     */
    public static Path getCachedFile(Path path) {
        if (path == null) {
            return null;
        }
        path = path.toAbsolutePath();

        Path cachedFile = ImageCache.getCacheDir()
                .resolve(getMD5(path.toString()) + "." + Utils.getExtension(path));
        if (Files.exists(cachedFile)) {
            LOGGER.trace("found cached file :) " + path);
            return cachedFile;
        }

        // TODO: when does this happen?!?!
        // is the path already inside the cache dir? serve direct
        if (path.startsWith(CACHE_DIR.toAbsolutePath())) {
            return path;
        }

        // is the image cache activated?
        if (!Globals.settings.isImageCache()) {
            LOGGER.trace("ImageCache not activated - return original file 1:1");
            return path;
        }

        try {
            Path p = ImageCache.cacheImage(path);
            LOGGER.trace("cached file successfully :) " + p);
            return p;
        } catch (EmptyFileException e) {
            LOGGER.warn("failed to cache file (file is empty): " + path);
        } catch (FileNotFoundException e) {
            LOGGER.trace(e.getMessage());
        } catch (Exception e) {
            LOGGER.warn("problem caching file: " + e.getMessage());
        }

        // fallback
        return path;
    }

    /**
     * Check whether the original image is in the image cache or not
     * 
     * @param path
     *          the path to the original image
     * @return true/false
     */
    public static boolean isImageCached(Path path) {
        if (!Globals.settings.isImageCache()) {
            return false;
        }

        Path cachedFile = CACHE_DIR.resolve(ImageCache.getMD5(path.toString()) + "." + Utils.getExtension(path));
        if (Files.exists(cachedFile)) {
            return true;
        }

        return false;
    }

    /**
     * clear the image cache for all graphics within the given media entity
     * 
     * @param entity
     *          the media entity
     */
    public static void clearImageCacheForMediaEntity(MediaEntity entity) {
        List<MediaFile> mediaFiles = new ArrayList<>(entity.getMediaFiles());
        for (MediaFile mediaFile : mediaFiles) {
            if (mediaFile.isGraphic()) {
                Path file = ImageCache.getCachedFile(mediaFile.getFileAsPath());
                if (Files.exists(file)) {
                    Utils.deleteFileSafely(file);
                }
            }
        }
    }

    /**
     * calculate a new size which fits into maxWidth and maxHeight
     * 
     * @param maxWidth
     *          the maximum width of the result
     * @param maxHeight
     *          the maximum height of the result
     * @param originalWidth
     *          the width of the source
     * @param originalHeight
     *          the height of the source
     * @param respectFactor
     *          should we respect the aspect ratio?
     * @return the calculated new size
     */
    public static Point calculateSize(int maxWidth, int maxHeight, int originalWidth, int originalHeight,
            boolean respectFactor) {
        Point size = new Point();
        if (respectFactor) {
            // calculate on available height
            size.y = maxHeight;
            size.x = (int) (size.y * (double) originalWidth / (double) originalHeight);

            if (size.x > maxWidth) {
                // calculate on available height
                size.x = maxWidth;
                size.y = (int) (size.x * (double) originalHeight / (double) originalWidth);
            }
        } else {
            size.x = maxWidth;
            size.y = maxHeight;
        }
        return size;
    }

    public static BufferedImage createImage(byte[] imageData) throws Exception {
        return createImage(Toolkit.getDefaultToolkit().createImage(imageData));
    }

    public static BufferedImage createImage(Path file) throws Exception {
        return createImage(Toolkit.getDefaultToolkit().createImage(file.toFile().getAbsolutePath()));
    }

    public static BufferedImage createImage(Image img) {
        return ImageLoader.createImage(img);
    }
}