All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.systemTray.util.ImageResizeUtil Maven / Gradle / Ivy

Go to download

Cross-platform SystemTray support for Swing/AWT, GtkStatusIcon, and AppIndicator on Java 8+

The newest version!
/*
 * Copyright 2023 dorkbox, llc
 *
 * 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 dorkbox.systemTray.util;

import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;

import dorkbox.systemTray.SystemTray;
import dorkbox.util.CacheUtil;
import dorkbox.util.IO;
import dorkbox.util.ImageUtil;

public
class ImageResizeUtil {

    // very simple regex.
    private static final Pattern JAR_URL_REGEX = Pattern.compile(".*.jar!.*");

    // - appIndicator/gtk require strings (which is the path)
    // - swing version loads as an image (which can be stream or path, we use path)
    private final CacheUtil cache;

    public ImageResizeUtil(CacheUtil cache) {
        this.cache = cache;
    }

    public
    File getTransparentImage() {
        // here, it doesn't matter what size the image is, as long as there is an image, the text in the menu will be shifted correctly
        // it is HIGHLY unlikely that the menu entry will be smaller than 4px.
        return getTransparentImage(4);
    }

    public
    File getTransparentImage(final int imageSize) {
        // NOTE: this does not need to be called on the EDT
        try {
            final File newFile = cache.create(imageSize + "_empty.png");
            return ImageUtil.createImage(imageSize, newFile, null);
        } catch (IOException e) {
            throw new RuntimeException("Unable to generate transparent image! Something is severely wrong!");
        }
    }

    public synchronized
    File getErrorImage(int size) {
        if (size <= 0) {
            // default size
            size = 32;
        }

        try {
            @SuppressWarnings("ConstantConditions")
            InputStream imageStream = ImageResizeUtil.class.getResource("error_32.png").openStream();

            // have to resize the image to be whatever size we specify
            imageStream = makeByteArrayInputStream(imageStream);
            imageStream.mark(0);

            // check if we already have this file information saved to disk, based on size + hash of data
            final String cacheName = size + "_" + CacheUtil.Companion.createNameAsHash(imageStream);
            ((ByteArrayInputStream) imageStream).reset();  // casting to avoid unnecessary try/catch for IOException


            // if we already have this fileName, reuse it
            final File check = cache.check(cacheName);
            if (check != null) {
                return check;
            }

            // we have to hop through hoops.
            File resizedFile = resizeFileNoCheck(size, imageStream);

            // now cache that file
            return cache.save(cacheName, resizedFile);
        } catch (Exception e) {
            // this must be thrown
            throw new RuntimeException("Serious problems! Unable to extract error image, this should NEVER happen!", e);
        }
    }

    private
    File resizeAndCache(final int size, final File file) {
        return resizeAndCache(size, file.getAbsolutePath());
    }

    private
    File resizeAndCache(final int size, final String fileName) {
        if (fileName == null) {
            return null;
        }

        if (SystemTray.DEBUG) {
            SystemTray.logger.debug("Resizing image to " + size + " : " + fileName);
        }

        InputStream inputStream = null;
        try {
            // if this is a JAR path, we have to load that. It is entirely possible that the PATH to a
            // resource (instead of the resource itself) is passed in.


            if (JAR_URL_REGEX.matcher(fileName)
                             .matches()) {
                // this is a JAR path, not a normal string!

                URL jarResource = new URL(fileName);
                inputStream = jarResource.openStream();
            } else {
                inputStream = new FileInputStream(fileName);
            }

            return resizeAndCache(size, inputStream);
        } catch (Exception e) {
            // have to serve up the error image instead.
            SystemTray.logger.error("Error reading image. Using error icon instead", e);
            return getErrorImage(size);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    @SuppressWarnings("Duplicates")
    private synchronized
    File resizeAndCache(final int size, InputStream imageStream) {
        if (imageStream == null) {
            return null;
        }

        final String cacheName;

        // no cached file, so we resize then save the new one.
        boolean needsResize = true;
        try {
            imageStream = makeByteArrayInputStream(imageStream);
            imageStream.mark(0);

            // check if we already have this file information saved to disk, based on size + hash of data
            cacheName = size + "_" + CacheUtil.Companion.createNameAsHash(imageStream);
            ((ByteArrayInputStream) imageStream).reset();  // casting to avoid unnecessary try/catch for IOException


            // if we already have this fileName, reuse it
            final File check = cache.check(cacheName);
            if (check != null && check.canRead()) {
                return check;
            }


            imageStream.mark(0);
            Dimension imageSize = ImageUtil.getImageSize(imageStream);
            //noinspection NumericCastThatLosesPrecision
            if (size == (int) imageSize.getHeight() && size == (int) imageSize.getWidth()) {
                // we can reuse this URL (it's the correct size).
                needsResize = false;
            }
        } catch (Exception e) {
            // have to serve up the error image instead.
            SystemTray.logger.error("Error getting image size. Using error icon instead", e);
            return getErrorImage(size);
        } finally {
            ((ByteArrayInputStream) imageStream).reset();  // casting to avoid unnecessary try/catch for IOException
        }



        if (needsResize) {
            // we have to hop through hoops.
            try {
                File resizedFile = resizeFileNoCheck(size, imageStream);

                // now cache that file
                try {
                    return cache.save(cacheName, resizedFile);
                } catch (Exception e) {
                    // have to serve up the error image instead.
                    SystemTray.logger.error("Error caching image. Using error icon instead", e);
                    return getErrorImage(size);
                }

            } catch (Exception e) {
                // have to serve up the error image instead.
                SystemTray.logger.error("Error resizing image. Using error icon instead", e);
                return getErrorImage(size);
            }

        } else {
            // no resize necessary, just cache as is.
            try {
                return cache.save(cacheName, imageStream);
            } catch (Exception e) {
                // have to serve up the error image instead.
                SystemTray.logger.error("Error caching image. Using error icon instead", e);
                return getErrorImage(size);
            }
        }
    }

    // if this input stream is NOT a ByteArrayInputStream, make it one.
    private static
    InputStream makeByteArrayInputStream(InputStream imageStream) throws IOException {
        if (!(imageStream instanceof ByteArrayInputStream)) {
            // have to make a copy of the inputStream, but only if necessary
            ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
            imageStream.close();

            imageStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        }
        return imageStream;
    }


    /**
     * Resizes the given InputStream to the specified height. No checks are performed if it's the correct height to begin with.
     *
     * Additionally, the image is scaled to where it's largest dimension will always be <= to the size.
     *
     * @return the file on disk that is the resized icon
     */
    @SuppressWarnings("ResultOfMethodCallIgnored")
    private
    File resizeFileNoCheck(final int size, InputStream inputStream) throws IOException {
        // have to resize the file (and return the new path)

        File newFile = cache.create("temp_resize.png");
        // if it's already there, we have to delete it
        newFile.delete();

        Image image = ImageIO.read(inputStream);
        ImageUtil.waitForImageLoad(image);

        BufferedImage bufferedImage = ImageUtil.getBufferedImage(image);

        // resize the image, keep aspect ratio
        int width = bufferedImage.getWidth();
        int height = bufferedImage.getHeight();
        if (width > height) {
            bufferedImage = ImageUtil.resizeImage(bufferedImage, size, -1);
        }
        else {
            bufferedImage = ImageUtil.resizeImage(bufferedImage, -1, size);
        }

        // make the image "square" so there is padding on the sides that are smaller
        bufferedImage = ImageUtil.getSquareBufferedImage(bufferedImage);

        // now write out the new one
        ImageIO.write(bufferedImage, "png", newFile); // made up extension

        return newFile;
    }


    public
    File shouldResizeOrCache(final boolean isTrayImage, final File imageFile) {
        if (imageFile == null) {
            return null;
        }

        if (SystemTray.AUTO_SIZE) {
            int size = getSize(isTrayImage);
            if (SystemTray.DEBUG) {
                SystemTray.logger.debug("Resizing image to " + size + " : " + imageFile);
            }

            return resizeAndCache(size, imageFile);
        } else {
            return imageFile;
        }
    }


    public
    File shouldResizeOrCache(final boolean isTrayImage, final String imagePath) {
        if (imagePath == null) {
            return null;
        }

        if (SystemTray.AUTO_SIZE) {
            int size = getSize(isTrayImage);

            if (SystemTray.DEBUG) {
                SystemTray.logger.debug("Resizing image to " + size + " : " + imagePath);
            }

            return resizeAndCache(size, imagePath);
        } else {
            return new File(imagePath);
        }
    }

    public
    File shouldResizeOrCache(final boolean isTrayImage, final URL imageUrl) {
        if (imageUrl == null) {
            return null;
        }

        int size = getSize(isTrayImage);

        try {
            if (SystemTray.AUTO_SIZE) {
                if (SystemTray.DEBUG) {
                    SystemTray.logger.debug("Resizing image to " + size + " : " + imageUrl);
                }

                InputStream inputStream = imageUrl.openStream();
                File file = resizeAndCache(size, inputStream);
                inputStream.close();

                return file;
            } else {
                return cache.save(imageUrl);
            }
        } catch (Exception e) {
            // have to serve up the error image instead.
            SystemTray.logger.error("Error reading image. Using error icon instead", e);
            return getErrorImage(size);
        }
    }

    public
    File shouldResizeOrCache(final boolean isTrayImage, final InputStream imageStream) {
        if (imageStream == null) {
            return null;
        }

        int size = getSize(isTrayImage);
        if (SystemTray.AUTO_SIZE) {
            if (SystemTray.DEBUG) {
                SystemTray.logger.debug("Resizing image-stream to " + size);
            }
            return resizeAndCache(size, imageStream);
        } else {
            try {
                return cache.save(imageStream);
            } catch (IOException e) {
                SystemTray.logger.error("Error checking cache for information. Using error icon instead", e);
                return getErrorImage(size);
            }
        }
    }


    public
    File shouldResizeOrCache(final boolean isTrayImage, final Image image) {
        if (image == null) {
            return null;
        }

        int size = getSize(isTrayImage);

        try {
            ImageUtil.waitForImageLoad(image);
            BufferedImage bufferedImage = ImageUtil.getBufferedImage(image);

            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "png", os);
            InputStream imageInputStream = new ByteArrayInputStream(os.toByteArray());


            File file;
            if (SystemTray.AUTO_SIZE) {
                if (SystemTray.DEBUG) {
                    SystemTray.logger.debug("Resizing image to " + size);
                }
                file = resizeAndCache(size, imageInputStream);
            } else {
                file = cache.save(imageInputStream);
            }

            imageInputStream.close(); // ByteArrayOutputStream doesn't do anything, but here for completeness + documentation
            return file;
        } catch (Exception e) {
            // have to serve up the error image instead.
            SystemTray.logger.error("Error reading image. Using error icon instead", e);
            return getErrorImage(size);
        }
    }

    public
    File shouldResizeOrCache(final boolean isTrayImage, final ImageInputStream imageStream) {
        if (imageStream == null) {
            return null;
        }

        int size = getSize(isTrayImage);
        try {
            ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
            ByteArrayInputStream fileStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());

            if (SystemTray.AUTO_SIZE) {
                if (SystemTray.DEBUG) {
                    SystemTray.logger.debug("Resizing image-stream to " + size);
                }
                return resizeAndCache(size, fileStream);
            } else {
                return cache.save(fileStream);
            }
        } catch (Exception e) {
            // have to serve up the error image instead.
            SystemTray.logger.error("Error reading image. Using error icon instead", e);
            return getErrorImage(size);
        }
    }

    private static
    int getSize(final boolean isTrayImage) {
        int size;
        if (isTrayImage) {
            // system tray image
            size = SizeAndScaling.TRAY_SIZE;
        } else {
            // menu image
            size = SizeAndScaling.TRAY_MENU_SIZE;
        }

        return size;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy