001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util;
017
018import java.awt.Graphics;
019import java.awt.image.BufferedImage;
020import java.io.ByteArrayInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024
025import javax.imageio.ImageIO;
026
027import org.apache.commons.io.IOUtils;
028
029import net.coobird.thumbnailator.ThumbnailParameter;
030import net.coobird.thumbnailator.Thumbnails;
031import net.coobird.thumbnailator.filters.ImageFilter;
032import net.coobird.thumbnailator.resizers.DefaultResizerFactory;
033import net.coobird.thumbnailator.tasks.io.InputStreamImageSource;
034import net.coobird.thumbnailator.tasks.io.OutputStreamImageSink;
035
036/**
037 * Helper for manipulating images.
038 */
039public final class ImageHelper
040{
041    private ImageHelper()
042    {
043        // empty constructor
044    }
045    
046    /**
047     * Returns a BufferedImage as from the supplied input stream
048     * @param is The input stream
049     * @return The buffered image
050     * @throws IOException if an error occurs during reading.
051     */
052    public static BufferedImage read(InputStream is) throws IOException
053    {
054        InputStreamImageSource imageSource = new InputStreamImageSource(is);
055        ThumbnailParameter param = new ThumbnailParameter(1.0f, 1.0f, null, true, null, null, 1.0f, 0, null, DefaultResizerFactory.getInstance(), true, true);
056        imageSource.setThumbnailParameter(param);
057        
058        BufferedImage src = imageSource.read();
059        
060        // Perform the image filters
061        for (ImageFilter filter : param.getImageFilters())
062        {
063            src = filter.apply(src);
064        }
065        
066        return src;
067    }
068    
069    /**
070     * Generates a thumbnail from a source InputStream. Note that if final width and height are equals to source width and height, the stream is just copied.
071     * @param is the source.
072     * @param os the destination.
073     * @param format the image format. Must be one of "gif", "png" or "jpg".
074     * @param height the specified height. Ignored if negative.
075     * @param width the specified width. Ignored if negative.
076     * @param maxHeight the maximum image height. Ignored if height or width is specified.
077     * @param maxWidth the maximum image width. Ignored if height or width is specified.
078     * @throws IOException if an error occurs when manipulating streams.
079     */
080    public static void generateThumbnail(InputStream is, OutputStream os, String format, int height, int width, int maxHeight, int maxWidth) throws IOException
081    {
082        BufferedImage src = read(is);
083        BufferedImage dest = _resizeImage(src, height, width, maxHeight, maxWidth);
084        
085        OutputStreamImageSink imageSink = new OutputStreamImageSink(os);
086        imageSink.setOutputFormatName(format);
087        imageSink.write(dest);
088    }
089    
090    /**
091     * Generates a BufferedImage with specified size instructions, scaling if necessary.<br>
092     * @param src the source image.
093     * @param height the specified height. Ignored if negative.
094     * @param width the specified width. Ignored if negative.
095     * @param maxHeight the maximum image height. Ignored if height or width is specified.
096     * @param maxWidth the maximum image width. Ignored if height or width is specified.
097     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
098     * @throws IOException If the source image is not readable
099     */
100    public static BufferedImage generateThumbnail(BufferedImage src, int height, int width, int maxHeight, int maxWidth) throws IOException
101    {
102        return _resizeImage(src, height, width, maxHeight, maxWidth);
103    }
104
105    /**
106     * Crop an image from a source InputStream. Note that if any of the coordinate and size are negatives, the image will just be copied.
107     * @param is the source.
108     * @param os the destination.
109     * @param format the image format. Must be one of "gif", "png" or "jpg".
110     * @param x The X coordinate of the upper-left corner of the specified rectangular region
111     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
112     * @param height the width of the specified rectangular region
113     * @param width the height of the specified rectangular region
114     * @throws IOException If an error occurs
115     */
116    public static void generateCroppedImage(InputStream is, OutputStream os, String format, int x, int y, int height, int width) throws IOException
117    {
118        byte[] fileContent = IOUtils.toByteArray(is); // keep a copy of the initial stream in case no thumbnail is necessary
119        
120        BufferedImage src = read(new ByteArrayInputStream(fileContent));
121        BufferedImage dest = _cropImage(src, x, y, height, width);
122        
123        if (src == dest)
124        {
125            // Thumbnail is equals to src image, means that the image is the same
126            // We'd rather like return the initial stream
127            IOUtils.write(fileContent, os);
128        }
129        else
130        {
131            ImageIO.write(dest, format, os);
132        }
133    }
134    
135    /**
136     * Crop the image by a specified rectangular region. The returned <code>BufferedImage</code> shares the same data array as the original image.
137     * @param src the source image.
138     * @param x The X coordinate of the upper-left corner of the specified rectangular region
139     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
140     * @param height the width of the specified rectangular region
141     * @param width the height of the specified rectangular region
142     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
143     * @throws IOException If the source image is not readable
144     */
145    public static BufferedImage generateCroppedImage(BufferedImage src, int x, int y, int height, int width) throws IOException
146    {
147        return _cropImage(src, x, y, height, width);
148    }
149    
150    /**
151     * Crop the image by a specified rectangular region. The returned <code>BufferedImage</code> shares the same data array as the original image.
152     * @param src the source image.
153     * @param x The X coordinate of the upper-left corner of the specified rectangular region
154     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
155     * @param height the width of the specified rectangular region
156     * @param width the height of the specified rectangular region
157     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
158     * @throws IOException If the source image is not readable
159     */
160    protected static BufferedImage _cropImage(BufferedImage src, int x, int y, int height, int width) throws IOException
161    {
162        BufferedImage dest = new BufferedImage(Integer.valueOf(width), Integer.valueOf(height), src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
163        Graphics g = dest.getGraphics();
164        int offsetX = x < 0 ? x * -1 : 0;
165        int offsetY = y < 0 ? y * -1 : 0;
166        g.drawImage(src, offsetX, offsetY, width, height, x + offsetX, y + offsetY, x + offsetX + width, y + offsetY + height, null);
167        g.dispose();
168        return dest;
169    }
170    
171    /**
172     * Resize the buffered image
173     * @param src the source image
174     * @param height the specified height. Ignored if negative.
175     * @param width the specified width. Ignored if negative.
176     * @param maxHeight the maximum image height. Ignored if height or width is specified.
177     * @param maxWidth the maximum image width. Ignored if height or width is specified.
178     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
179     * @throws IOException If the source image is not readable
180     */
181    protected static BufferedImage _resizeImage(BufferedImage src, int height, int width, int maxHeight, int maxWidth) throws IOException
182    {
183        int srcHeight = src.getHeight();
184        int srcWidth = src.getWidth();
185        
186        int destHeight = 0;
187        int destWidth = 0;
188        
189        boolean keepAspectRatio = true;
190        
191        if (height > 0)
192        {
193            // heigth is specified
194            destHeight = height;
195            
196            if (width > 0)
197            {
198                // additionnally, width is also specified
199                destWidth = width;
200                keepAspectRatio = false;
201            }
202            else
203            {
204                // width is computed
205                destWidth = srcWidth * destHeight / srcHeight;
206            }
207        }
208        else if (width > 0)
209        {
210            // width is specified, height is computed
211            destWidth = width;
212            destHeight = srcHeight * destWidth / srcWidth;
213        }
214        else if (maxHeight > 0)
215        {
216            if (maxWidth > 0)
217            {
218                if (srcHeight <= maxHeight && srcWidth <= maxWidth)
219                {
220                    // the source image is already smaller than the destination box
221//                    return thumbnail.scale(1);
222                    return src;
223                }
224                
225                destWidth = maxWidth;
226                destHeight = maxHeight;
227            }
228            else
229            {
230                if (srcHeight <= maxHeight)
231                {
232                    // the source image is already smaller than the destination box
233                    return src;
234                }
235                
236                destHeight = maxHeight;
237                destWidth = srcWidth * destHeight / srcHeight;
238            }
239        }
240        else if (maxWidth > 0)
241        {
242            if (srcWidth <= maxWidth)
243            {
244                // the source image is already smaller than the destination box
245                return src;
246            }
247            
248            destWidth = maxWidth;
249            destHeight = srcHeight * destWidth / srcWidth;
250        }
251        else
252        {
253            // No resize is required
254            return src;
255        }
256        
257        if (destHeight == srcHeight && destWidth == srcWidth)
258        {
259            // already the good format, don't change anything
260            return src;
261        }
262        
263        return Thumbnails.of(src).size(destWidth, destHeight).keepAspectRatio(keepAspectRatio).imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB).asBufferedImage();
264    }
265}