org.olat.core.commons.services.image.spi.ImageHelperImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.olat.core.commons.services.image.spi.ImageHelperImpl.java

Source

/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.  
* <p>
*/

package org.olat.core.commons.services.image.spi;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.color.CMMException;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;

import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.olat.core.commons.services.image.Crop;
import org.olat.core.commons.services.image.Size;
import org.olat.core.commons.services.thumbnail.CannotGenerateThumbnailException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.FileUtils;
import org.olat.core.util.StringHelper;
import org.olat.core.util.WorkThreadInformations;
import org.olat.core.util.vfs.LocalFileImpl;
import org.olat.core.util.vfs.VFSLeaf;

// FIXME:as:c google for deployment of servers with no X installed (fj)
// see also
// http://java.sun.com/j2se/1.5.0/docs/guide/awt/AWTChanges.html#headless
/**
 * Helper class which scale an image and saved it as jpeg. Input format are
 * the ones supported by standard java: gif, jpg, png.
 * 
 * @author Alexander Schneider, srosse
 */
public class ImageHelperImpl extends AbstractImageHelper {

    private static final OLog log = Tracing.createLoggerFor(ImageHelperImpl.class);

    private static final String OUTPUT_FORMAT = "jpeg";

    @Override
    public Size thumbnailPDF(VFSLeaf pdfFile, VFSLeaf thumbnailFile, int maxWidth, int maxHeight) {
        InputStream in = null;
        PDDocument document = null;
        try {
            WorkThreadInformations.setInfoFiles(null, pdfFile);
            WorkThreadInformations.set("Generate thumbnail VFSLeaf=" + pdfFile);
            in = pdfFile.getInputStream();
            document = PDDocument.load(in);
            if (document.isEncrypted()) {
                try {
                    document.decrypt("");
                } catch (Exception e) {
                    log.info("PDF document is encrypted: " + pdfFile);
                    throw new CannotGenerateThumbnailException("PDF document is encrypted: " + pdfFile);
                }
            }
            List pages = document.getDocumentCatalog().getAllPages();
            PDPage page = (PDPage) pages.get(0);
            BufferedImage image = page.convertToImage(BufferedImage.TYPE_INT_BGR, 72);
            Size size = scaleImage(image, thumbnailFile, maxWidth, maxHeight);
            if (size != null) {
                return size;
            }
            return null;
        } catch (CannotGenerateThumbnailException e) {
            return null;
        } catch (Exception e) {
            log.warn("Unable to create image from pdf file.", e);
            return null;
        } finally {
            WorkThreadInformations.unset();
            FileUtils.closeSafely(in);
            if (document != null) {
                try {
                    document.close();
                } catch (IOException e) {
                    //only a try, fail silently
                }
            }
        }
    }

    @Override
    public boolean cropImage(File image, File cropedImage, Crop cropSelection) {
        ImageInputStream imageSrc = null;
        try {
            imageSrc = new FileImageInputStream(image);
            String extension = FileUtils.getFileSuffix(cropedImage.getName());
            SizeAndBufferedImage img = getImage(imageSrc, extension);
            BufferedImage croppedImg = cropTo(img.getImage(), img.getSize(), cropSelection);
            Size size = new Size(cropSelection.getWidth(), cropSelection.getHeight(), false);
            return writeTo(croppedImg, cropedImage, size, extension);
        } catch (IOException e) {
            return false;
            //fxdiff FXOLAT-109: prevent red screen if the image has wrong EXIF data
        } catch (CMMException e) {
            return false;
        } finally {
            closeQuietly(imageSrc);
        }
    }

    /**
     * @param image the image to scale
     * @param scaledImaged the new scaled image
     * @param maxSize the maximum size (height or width) of the new scaled image
     * @return
     */
    @Override
    public Size scaleImage(File image, String imageExt, VFSLeaf scaledImage, int maxWidth, int maxHeight) {

        ImageInputStream imageIns = null;
        OutputStream bos = new BufferedOutputStream(scaledImage.getOutputStream(false));
        try {
            imageIns = new FileImageInputStream(image);
            SizeAndBufferedImage scaledSize = calcScaledSize(imageIns, imageExt, maxWidth, maxHeight, false);
            if (scaledSize == null) {
                return null;
            }
            if (!scaledSize.getScaledSize().isChanged() && isSameFormat(image, scaledImage)) {
                InputStream cloneIns = new FileInputStream(image);
                IOUtils.copy(cloneIns, bos);
                IOUtils.closeQuietly(cloneIns);
                return scaledSize.getScaledSize();
            } else {
                BufferedImage imageSrc = scaledSize.getImage();
                if (imageSrc == null) {
                    // happens with faulty Java implementation, e.g. on MacOSX Java 10, or
                    // unsupported image format
                    return null;
                }
                BufferedImage scaledBufferedImage = scaleTo(imageSrc, scaledSize.getScaledSize());
                if (writeTo(scaledBufferedImage, bos, scaledSize.getScaledSize(), getImageFormat(scaledImage))) {
                    return scaledSize.getScaledSize();
                }
                return null;
            }
        } catch (IOException e) {
            return null;
        } finally {
            closeQuietly(imageIns);
            FileUtils.closeSafely(bos);
        }
    }

    /**
     * @param image the image to scale
     * @param scaledImaged the new scaled image
     * @param maxSize the maximum size (height or width) of the new scaled image
     * @return
     */
    @Override
    public Size scaleImage(VFSLeaf image, VFSLeaf scaledImage, int maxWidth, int maxHeight, boolean fill) {
        OutputStream bos = null;
        ImageInputStream ins = null;
        try {
            ins = getInputStream(image);
            if (ins == null) {
                return null;
            }
            String extension = FileUtils.getFileSuffix(image.getName());
            SizeAndBufferedImage scaledSize = calcScaledSize(ins, extension, maxWidth, maxHeight, fill);
            if (scaledSize == null || scaledSize.getImage() == null) {
                return null;
            }

            ins = getInputStream(image);
            if (ins == null) {
                return null;
            }
            bos = new BufferedOutputStream(scaledImage.getOutputStream(false));
            if (!scaledSize.getScaledSize().isChanged() && isSameFormat(image, scaledImage)) {
                InputStream cloneIns = image.getInputStream();
                IOUtils.copy(cloneIns, bos);
                IOUtils.closeQuietly(cloneIns);
                return scaledSize.getScaledSize();
            } else {
                BufferedImage imageSrc = scaledSize.getImage();
                BufferedImage scaledSrc = scaleTo(imageSrc, scaledSize.getScaledSize());
                boolean scaled = writeTo(scaledSrc, bos, scaledSize.getScaledSize(), getImageFormat(scaledImage));
                if (scaled) {
                    return scaledSize.getScaledSize();
                }
                return null;
            }
        } catch (IOException e) {
            return null;
            //fxdiff FXOLAT-109: prevent red screen if the image has wrong EXIF data
        } catch (CMMException e) {
            return null;
        } finally {
            closeQuietly(ins);
            FileUtils.closeSafely(bos);
        }
    }

    /**
     * 
     * @param leaf
     * @return
     */
    private ImageInputStream getInputStream(VFSLeaf leaf) throws IOException {
        if (leaf == null) {
            return null;
        }
        if (leaf instanceof LocalFileImpl) {
            LocalFileImpl file = (LocalFileImpl) leaf;
            if (file.getBasefile() != null) {
                return new FileImageInputStream(file.getBasefile());
            }
            return null;
        }
        InputStream ins = leaf.getInputStream();
        if (ins == null) {
            return null;
        }
        return new MemoryCacheImageInputStream(leaf.getInputStream());
    }

    private Size scaleImage(BufferedImage image, VFSLeaf scaledImage, int maxWidth, int maxHeight) {
        OutputStream bos = null;
        try {
            if (image == null) {
                // happens with faulty Java implementation, e.g. on MacOSX Java 10, or
                // unsupported image format
                return null;
            }
            bos = new BufferedOutputStream(scaledImage.getOutputStream(false));
            Size scaledSize = calcScaledSize(image, maxWidth, maxHeight);
            if (writeTo(scaleTo(image, scaledSize), bos, scaledSize, getImageFormat(scaledImage))) {
                return scaledSize;
            }
            return null;
        } catch (Exception e) {
            return null;
        } finally {
            FileUtils.closeSafely(bos);
        }
    }

    /**
     * @param image The image to scale
     * @param imageExt The extension if not given by the image file (optional)
     * @param scaledImaged the new scaled image
     * @param maxWidth the maximum width of the new scaled image
     * @param maxheight the maximum height of the new scaled image
     * @return
     */
    @Override
    public Size scaleImage(File image, String imageExt, File scaledImage, int maxWidth, int maxHeight,
            boolean fill) {
        ImageInputStream imageSrc = null;
        try {
            imageSrc = new FileImageInputStream(image);
            SizeAndBufferedImage scaledSize = calcScaledSize(imageSrc, imageExt, maxWidth, maxHeight, fill);
            if (scaledSize == null || scaledSize.image == null) {
                return null;
            }
            if (!scaledSize.getScaledSize().isChanged() && isSameFormat(image, imageExt, scaledImage)) {
                if (FileUtils.copyFileToFile(image, scaledImage, false)) {
                    return scaledSize.getSize();
                }
            }
            BufferedImage bufferedImage = scaledSize.image;
            BufferedImage scaledBufferedImage = scaleTo(bufferedImage, scaledSize.getScaledSize());
            if (writeTo(scaledBufferedImage, scaledImage, scaledSize.getScaledSize(),
                    getImageFormat(scaledImage))) {
                return scaledSize.getScaledSize();
            }
            return null;
        } catch (IOException e) {
            return null;
            //fxdiff FXOLAT-109: prevent red screen if the image has wrong EXIF data
        } catch (CMMException e) {
            return null;
        } finally {
            closeQuietly(imageSrc);
        }
    }

    private static String getImageFormat(File image) {
        String extension = FileUtils.getFileSuffix(image.getName());
        if (StringHelper.containsNonWhitespace(extension)) {
            return extension.toLowerCase();
        }
        return OUTPUT_FORMAT;
    }

    private static String getImageFormat(VFSLeaf image) {
        String extension = FileUtils.getFileSuffix(image.getName());
        if (StringHelper.containsNonWhitespace(extension)) {
            return extension.toLowerCase();
        }
        return OUTPUT_FORMAT;
    }

    private static boolean isSameFormat(File source, VFSLeaf scaled) {
        String sourceExt = FileUtils.getFileSuffix(source.getName());
        String scaledExt = getImageFormat(scaled);
        if (sourceExt != null && sourceExt.equals(scaledExt)) {
            return true;
        }
        return false;
    }

    private static boolean isSameFormat(VFSLeaf source, VFSLeaf scaled) {
        String sourceExt = FileUtils.getFileSuffix(source.getName());
        String scaledExt = getImageFormat(scaled);
        if (sourceExt != null && sourceExt.equals(scaledExt)) {
            return true;
        }
        return false;
    }

    private static boolean isSameFormat(File source, String sourceExt, File scaled) {
        if (!StringHelper.containsNonWhitespace(sourceExt)) {
            sourceExt = FileUtils.getFileSuffix(source.getName());
        }
        String scaledExt = getImageFormat(scaled);
        if (sourceExt != null && sourceExt.equals(scaledExt)) {
            return true;
        }
        return false;
    }

    /**
     * Calculate the size of the new image. The method keep the ratio and doesn't
     * scale up the image.
     * @param image the image to scale
     * @param maxWidth the maximum width of the new scaled image
     * @param maxheight the maximum height of the new scaled image
     * @return
     */
    public static Size calcScaledSize(BufferedImage image, int maxWidth, int maxHeight) {
        int width = image.getWidth();
        int height = image.getHeight();
        return computeScaledSize(width, height, maxWidth, maxHeight, false);
    }

    private static SizeAndBufferedImage calcScaledSize(ImageInputStream stream, String suffix, int maxWidth,
            int maxHeight, boolean fill) {
        Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix);
        if (iter.hasNext()) {
            ImageReader reader = iter.next();
            try {
                reader.setInput(stream, true, true);
                int width = reader.getWidth(reader.getMinIndex());
                int height = reader.getHeight(reader.getMinIndex());
                Size size = new Size(width, height, false);
                Size scaledSize = computeScaledSize(width, height, maxWidth, maxHeight, fill);
                SizeAndBufferedImage all = new SizeAndBufferedImage(size, scaledSize);

                int readerMinIndex = reader.getMinIndex();
                ImageReadParam param = reader.getDefaultReadParam();
                Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0);
                while (imageTypes.hasNext()) {
                    try {
                        ImageTypeSpecifier imageTypeSpecifier = imageTypes.next();
                        int bufferedImageType = imageTypeSpecifier.getBufferedImageType();
                        if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) {
                            param.setDestinationType(imageTypeSpecifier);
                        }

                        double memoryKB = (width * height * 4) / 1024d;
                        if (memoryKB > 2000) {// check limit at 20MB
                            double free = Runtime.getRuntime().freeMemory() / 1024d;
                            if (free > memoryKB) {
                                all.setImage(reader.read(readerMinIndex, param));
                            } else {
                                // make sub sampling to save memory
                                int ratio = (int) Math.round(Math.sqrt(memoryKB / free));
                                param.setSourceSubsampling(ratio, ratio, 0, 0);
                                all.setImage(reader.read(readerMinIndex, param));
                            }
                        } else {
                            all.setImage(reader.read(readerMinIndex, param));
                        }
                        return all;
                    } catch (IllegalArgumentException e) {
                        log.warn(e.getMessage(), e);
                    }
                }
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            } finally {
                reader.dispose();
            }
        } else {
            log.error("No reader found for given format: " + suffix, null);
        }
        return null;
    }

    private static SizeAndBufferedImage getImage(ImageInputStream stream, String suffix) {
        Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix);
        if (iter.hasNext()) {
            ImageReader reader = iter.next();
            try {
                reader.setInput(stream, true, true);
                int width = reader.getWidth(reader.getMinIndex());
                int height = reader.getHeight(reader.getMinIndex());
                Size size = new Size(width, height, false);
                SizeAndBufferedImage all = new SizeAndBufferedImage(size, null);

                int readerMinIndex = reader.getMinIndex();
                ImageReadParam param = reader.getDefaultReadParam();
                Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0);
                while (imageTypes.hasNext()) {
                    try {
                        ImageTypeSpecifier imageTypeSpecifier = imageTypes.next();
                        int bufferedImageType = imageTypeSpecifier.getBufferedImageType();
                        if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) {
                            param.setDestinationType(imageTypeSpecifier);
                        }
                        all.setImage(reader.read(readerMinIndex, param));
                        return all;
                    } catch (IllegalArgumentException e) {
                        log.warn(e.getMessage(), e);
                    }
                }
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            } finally {
                reader.dispose();
            }
        } else {
            log.error("No reader found for given format: " + suffix, null);
        }
        return null;
    }

    private static Size computeScaledSize(int width, int height, int maxWidth, int maxHeight, boolean fill) {
        if (maxHeight > height && maxWidth > width) {
            return new Size(width, height, 0, 0, false);
        }

        int xOffset = 0;
        int yOffset = 0;

        if (fill) {
            double thumbRatio = (double) maxWidth / (double) maxHeight;
            double imageRatio = (double) width / (double) height;
            if (thumbRatio < imageRatio) {
                int newWidth = (int) (maxHeight * imageRatio);
                if (newWidth > maxWidth) {
                    xOffset = (newWidth - maxWidth) / 2;
                }
                maxWidth = Math.min(maxWidth, newWidth);
            } else {
                int newHeight = (int) (maxWidth / imageRatio);
                if (newHeight > maxHeight) {
                    yOffset = (newHeight - maxHeight) / 2;
                }
                maxHeight = Math.min(maxHeight, newHeight);
            }
        } else {
            double thumbRatio = (double) maxWidth / (double) maxHeight;
            double imageRatio = (double) width / (double) height;
            if (thumbRatio < imageRatio) {
                maxHeight = (int) (maxWidth / imageRatio);
            } else {
                maxWidth = (int) (maxHeight * imageRatio);
            }
        }
        return new Size(maxWidth, maxHeight, xOffset, yOffset, true);
    }

    /**
     * Can change this to choose a better compression level as the default
     * @param image
     * @param scaledImage
     * @return
     */
    public static boolean writeTo(BufferedImage image, File scaledImage, Size scaledSize, String outputFormat) {
        try {
            if (!StringHelper.containsNonWhitespace(outputFormat)) {
                outputFormat = OUTPUT_FORMAT;
            }

            Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(outputFormat);
            if (writers.hasNext()) {
                ImageWriter writer = writers.next();
                ImageWriteParam iwp = getOptimizedImageWriteParam(writer, scaledSize);
                IIOImage iiOImage = new IIOImage(image, null, null);
                ImageOutputStream iOut = new FileImageOutputStream(scaledImage);
                writer.setOutput(iOut);
                writer.write(null, iiOImage, iwp);
                writer.dispose();
                iOut.flush();
                iOut.close();
                return true;
            } else {
                return ImageIO.write(image, outputFormat, scaledImage);
            }
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Can change this to choose a better compression level as the default
     * @param image
     * @param scaledImage
     * @return
     */
    private static boolean writeTo(BufferedImage image, OutputStream scaledImage, Size scaledSize,
            String outputFormat) {
        try {
            if (!StringHelper.containsNonWhitespace(outputFormat)) {
                outputFormat = OUTPUT_FORMAT;
            }

            Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(outputFormat);
            if (writers.hasNext()) {
                ImageWriter writer = writers.next();
                ImageWriteParam iwp = getOptimizedImageWriteParam(writer, scaledSize);
                IIOImage iiOImage = new IIOImage(image, null, null);
                ImageOutputStream iOut = new MemoryCacheImageOutputStream(scaledImage);
                writer.setOutput(iOut);
                writer.write(null, iiOImage, iwp);
                writer.dispose();
                iOut.flush();
                iOut.close();
                return true;
            } else {
                return ImageIO.write(image, outputFormat, scaledImage);
            }
        } catch (IOException e) {
            return false;
        }
    }

    private static ImageWriteParam getOptimizedImageWriteParam(ImageWriter writer, Size scaledSize) {
        ImageWriteParam iwp = writer.getDefaultWriteParam();
        try {
            if (iwp.canWriteCompressed()) {
                iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                int maxSize = Math.max(scaledSize.getWidth(), scaledSize.getHeight());
                if (maxSize <= 50) {
                    iwp.setCompressionQuality(0.95f);
                } else if (maxSize <= 100) {
                    iwp.setCompressionQuality(0.90f);
                } else if (maxSize <= 200) {
                    iwp.setCompressionQuality(0.85f);
                } else if (maxSize <= 500) {
                    iwp.setCompressionQuality(0.80f);
                } else {
                    iwp.setCompressionQuality(0.75f);
                }
            }
        } catch (Exception e) {
            //bmp can be compressed but don't allow it!!!
            return writer.getDefaultWriteParam();
        }
        return iwp;
    }

    private static BufferedImage scaleTo(BufferedImage image, Size scaledSize) {
        if (!scaledSize.isChanged())
            return image;
        return scaleFastTo(image, scaledSize);
    }

    private static BufferedImage cropTo(BufferedImage img, Size size, Crop cropSelection) {
        int w = cropSelection.getWidth();
        int h = cropSelection.getHeight();
        int x = cropSelection.getX();
        int y = cropSelection.getY();
        //make sure that the sub image is in the raster (if not boum!!)
        w = Math.min(w, size.getWidth() - x);
        h = Math.min(h, size.getHeight() - y);
        //crop the image to see only the center of the image
        return img.getSubimage(x, y, w, h);
    }

    /**
     * This code is very inspired on Chris Campbells article "The Perils of Image.getScaledInstance()"
     *
     * The article can be found here:
     * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
     *
     * Note that the filter method is threadsafe
     */
    private static BufferedImage scaleFastTo(BufferedImage img, Size scaledSize) {
        if (!scaledSize.isChanged())
            return img;

        BufferedImage dest;
        if (img.getType() == BufferedImage.TYPE_CUSTOM) {
            dest = new BufferedImage(scaledSize.getWidth(), scaledSize.getHeight(), BufferedImage.TYPE_INT_ARGB);
        } else {
            dest = new BufferedImage(scaledSize.getWidth(), scaledSize.getHeight(), img.getType());
        }

        int dstWidth = scaledSize.getWidth();
        int dstHeight = scaledSize.getHeight();

        BufferedImage ret = img;
        int w, h;

        // Use multi-step technique: start with original size, then
        // scale down in multiple passes with drawImage()
        // until the target size is reached
        w = img.getWidth();
        h = img.getHeight();

        int x = scaledSize.getXOffset();
        int y = scaledSize.getYOffset();

        //crop the image to see only the center of the image
        if (x > 0 || y > 0) {
            ret = img.getSubimage(x, y, w - (2 * x), h - (2 * y));
        }

        do {
            if (w > dstWidth) {
                if (x > 0) {
                    x /= 2;
                }
                w /= 2;
                if (w < dstWidth) {
                    w = dstWidth;
                }
            } else {
                w = dstWidth;
            }

            if (h > dstHeight) {
                h /= 2;
                if (h < dstHeight) {
                    h = dstHeight;
                }
            } else {
                h = dstHeight;
            }

            BufferedImage tmp;
            if (dest.getWidth() == w && dest.getHeight() == h && w == dstWidth && h == dstHeight) {
                tmp = dest;
            } else {
                tmp = new BufferedImage(w, h, dest.getType());
            }

            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2.drawImage(ret, 0, 0, w, h, null);
            g2.dispose();

            ret = tmp;
        } while (w != dstWidth || h != dstHeight);

        return ret;
    }

    private final static void closeQuietly(ImageInputStream ins) {
        if (ins != null) {
            try {
                ins.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static final class SizeAndBufferedImage {
        private Size size;
        private Size scaledSize;
        private BufferedImage image;

        public SizeAndBufferedImage(Size size, Size scaledSize) {
            this.size = size;
            this.scaledSize = scaledSize;
        }

        public Size getSize() {
            return size;
        }

        public Size getScaledSize() {
            return scaledSize;
        }

        public BufferedImage getImage() {
            return image;
        }

        public void setImage(BufferedImage image) {
            this.image = image;
        }
    }
}