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.image.BufferedImage;
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023
024import org.apache.commons.io.IOUtils;
025
026import net.coobird.thumbnailator.ThumbnailParameter;
027import net.coobird.thumbnailator.Thumbnails;
028import net.coobird.thumbnailator.filters.ImageFilter;
029import net.coobird.thumbnailator.geometry.Positions;
030import net.coobird.thumbnailator.resizers.DefaultResizerFactory;
031import net.coobird.thumbnailator.tasks.io.InputStreamImageSource;
032import net.coobird.thumbnailator.tasks.io.OutputStreamImageSink;
033
034/**
035 * Helper for manipulating images.
036 */
037public final class ImageHelper
038{
039    private static final float IMAGE_QUALITY = 0.9f;
040    
041    private ImageHelper()
042    {
043        // empty constructor
044    }
045    
046    private static boolean _needsChanges(int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth)
047    {
048        return height > 0 || width > 0 || maxHeight > 0 || maxWidth > 0 || cropHeight > 0 || cropWidth > 0;
049    }
050    
051    /**
052     * Returns a BufferedImage as from the supplied input stream
053     * @param is The input stream
054     * @return The buffered image
055     * @throws IOException if an error occurs during reading.
056     */
057    public static BufferedImage read(InputStream is) throws IOException
058    {
059        InputStreamImageSource imageSource = new InputStreamImageSource(is);
060        ThumbnailParameter param = new ThumbnailParameter(1.0f, 1.0f, null, true, null, null, 1.0f, 0, null, DefaultResizerFactory.getInstance(), true, true);
061        imageSource.setThumbnailParameter(param);
062        
063        BufferedImage src = imageSource.read();
064        
065        // Perform the image filters
066        for (ImageFilter filter : param.getImageFilters())
067        {
068            src = filter.apply(src);
069        }
070        
071        return src;
072    }
073    
074    /**
075     * 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.
076     * If the image should be both cropped and resized, the resizing will be done after the cropping.
077     * @param is the source.
078     * @param os the destination.
079     * @param format the image format. Must be one of "gif", "png" or "jpg".
080     * @param height the specified height. Ignored if negative.
081     * @param width the specified width. Ignored if negative.
082     * @param maxHeight the maximum image height. Ignored if height or width is specified.
083     * @param maxWidth the maximum image width. Ignored if height or width is specified.
084     * @param cropHeight the height of the cropped image. Ignore if negative.
085     * @param cropWidth the width of the cropped image. Ignore if negative.
086     * @throws IOException if an error occurs when manipulating streams.
087     */
088    public static void generateThumbnail(InputStream is, OutputStream os, String format, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) throws IOException
089    {
090        if (!_needsChanges(height, width, maxHeight, maxWidth, cropHeight, cropWidth))
091        {
092            // no resizing nor cropping needed
093            IOUtils.copy(is, os);
094            return;
095        }
096        
097        byte[] data = IOUtils.toByteArray(is);
098        BufferedImage src = read(new ByteArrayInputStream(data));
099        BufferedImage dest = src;
100        
101        if (cropHeight > 0 || cropWidth > 0)
102        {
103            dest = _cropImage(dest, cropHeight, cropWidth);
104        }
105        
106        if (height > 0 || width > 0 || maxHeight > 0 || maxWidth > 0)
107        {
108            dest = _resizeImage(dest, height, width, maxHeight, maxWidth);
109        }
110        
111        if (src == dest)
112        {
113            // source and destination have same dimensions, so preserve the initial image
114            IOUtils.copy(new ByteArrayInputStream(data), os);
115            return;
116        }
117        
118        OutputStreamImageSink imageSink = new OutputStreamImageSink(os);
119        imageSink.setOutputFormatName(format);
120        ThumbnailParameter param = new ThumbnailParameter(1.0f, 1.0f, null, true, null, null, IMAGE_QUALITY, 0, null, DefaultResizerFactory.getInstance(), true, true);
121        imageSink.setThumbnailParameter(param);
122        imageSink.write(dest);
123    }   
124    
125    /**
126     * Generates a BufferedImage with specified size instructions, scaling if necessary.<br>
127     * If the image should be both cropped and resized, the resizing will be done after the cropping.
128     * @param src the source image.
129     * @param height the specified height. Ignored if negative.
130     * @param width the specified width. Ignored if negative.
131     * @param maxHeight the maximum image height. Ignored if height or width is specified.
132     * @param maxWidth the maximum image width. Ignored if height or width is specified.
133     * @param cropHeight the height of the cropped image. Ignore if negative.
134     * @param cropWidth the width of the cropped image. Ignore if negative.
135     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
136     * @throws IOException If the source image is not readable
137     */
138    public static BufferedImage generateThumbnail(BufferedImage src, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) throws IOException
139    {
140        BufferedImage dest = src;
141        
142        if (cropHeight > 0 || cropWidth > 0)
143        {
144            dest = _cropImage(src, cropHeight, cropWidth);
145        }
146        
147        if (height > 0 || width > 0 || maxHeight > 0 || maxWidth > 0)
148        {
149            dest = _resizeImage(src, height, width, maxHeight, maxWidth);
150        }
151        
152        return dest;
153    }
154    
155    /**
156     * 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.
157     * @param is the source.
158     * @param os the destination.
159     * @param format the image format. Must be one of "gif", "png" or "jpg".
160     * @param height the specified height. Ignored if negative.
161     * @param width the specified width. Ignored if negative.
162     * @param maxHeight the maximum image height. Ignored if height or width is specified.
163     * @param maxWidth the maximum image width. Ignored if height or width is specified.
164     * @throws IOException if an error occurs when manipulating streams.
165     */
166    public static void generateThumbnail(InputStream is, OutputStream os, String format, int height, int width, int maxHeight, int maxWidth) throws IOException
167    {
168        generateThumbnail(is, os, format, height, width, maxHeight, maxWidth, 0, 0);
169    }
170    
171    /**
172     * Generates a BufferedImage with specified size instructions, scaling if necessary.<br>
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    public static BufferedImage generateThumbnail(BufferedImage src, int height, int width, int maxHeight, int maxWidth) throws IOException
182    {
183        return _resizeImage(src, height, width, maxHeight, maxWidth);
184    }
185    
186    /**
187     * 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.
188     * @param is the source.
189     * @param os the destination.
190     * @param format the image format. Must be one of "gif", "png" or "jpg".
191     * @param ratio ratio to resize the image (1 = same size)
192     * @throws IOException if an error occurs when manipulating streams.
193     */
194    public static void generateThumbnail(InputStream is, OutputStream os, String format, double ratio) throws IOException
195    {
196        if (ratio == 1)
197        {
198            // no resizing needed
199            IOUtils.copy(is, os);
200            return;
201        }
202        
203        BufferedImage src = read(is);
204        BufferedImage dest = _resizeImage(src, ratio);
205        
206        OutputStreamImageSink imageSink = new OutputStreamImageSink(os);
207        imageSink.setOutputFormatName(format);
208        ThumbnailParameter param = new ThumbnailParameter(1.0f, 1.0f, null, true, null, null, IMAGE_QUALITY, 0, null, DefaultResizerFactory.getInstance(), true, true);
209        imageSink.setThumbnailParameter(param);
210        imageSink.write(dest);
211    }
212    
213    /**
214     * Generates a BufferedImage with specified size instructions, scaling if necessary.<br>
215     * @param src the source image.
216     * @param ratio ratio to resize the image (1 = same size)
217     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
218     * @throws IOException If the source image is not readable
219     */
220    public static BufferedImage generateThumbnail(BufferedImage src, double ratio) throws IOException
221    {
222        return _resizeImage(src, ratio);
223    }
224
225    /**
226     * Crop an image from a source InputStream. Note that if any of the coordinate and size are negatives, the image will just be copied.
227     * @param is the source.
228     * @param os the destination.
229     * @param format the image format. Must be one of "gif", "png" or "jpg".
230     * @param x The X coordinate of the upper-left corner of the specified rectangular region
231     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
232     * @param height the width of the specified rectangular region
233     * @param width the height of the specified rectangular region
234     * @throws IOException If an error occurs
235     */
236    public static void generateCroppedImage(InputStream is, OutputStream os, String format, int x, int y, int height, int width) throws IOException
237    {
238        BufferedImage src = read(is);
239        BufferedImage dest = _cropImage(src, x, y, height, width);
240        
241        OutputStreamImageSink imageSink = new OutputStreamImageSink(os);
242        imageSink.setOutputFormatName(format);
243        ThumbnailParameter param = new ThumbnailParameter(1.0f, 1.0f, null, true, null, null, IMAGE_QUALITY, 0, null, DefaultResizerFactory.getInstance(), true, true);
244        imageSink.setThumbnailParameter(param);
245        imageSink.write(dest);
246    }
247    
248    /**
249     * Crop the image in the center, at the specified dimensions. The returned <code>BufferedImage</code> shares the same data array as the original image.
250     * @param src the source image.
251     * @param height the width of the specified rectangular region
252     * @param width the height of the specified rectangular region
253     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
254     * @throws IOException If the source image is not readable
255     */
256    public static BufferedImage generateCroppedImage(BufferedImage src, int height, int width) throws IOException
257    {
258        return _cropImage(src, height, width);
259    }    
260    /**
261     * Crop the image by a specified rectangular region. The returned <code>BufferedImage</code> shares the same data array as the original image.
262     * @param src the source image.
263     * @param x The X coordinate of the upper-left corner of the specified rectangular region
264     * @param y the Y coordinate of the upper-left corner of the specified rectangular region
265     * @param height the width of the specified rectangular region
266     * @param width the height of the specified rectangular region
267     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
268     * @throws IOException If the source image is not readable
269     */
270    public static BufferedImage generateCroppedImage(BufferedImage src, int x, int y, int height, int width) throws IOException
271    {
272        return _cropImage(src, x, y, height, width);
273    }
274    
275    /**
276     * Crop the image to the size specified to the center of the image.
277     * @param src the source image.
278     * @param height the width of the specified rectangular region
279     * @param width the height of the specified rectangular region
280     * @return The cropped image as a BufferedImage
281     * @throws IOException If the source image is not readable
282     */
283    protected static BufferedImage _cropImage(BufferedImage src, int height, int width) throws IOException
284    {
285        return Thumbnails.of(src).size(width, height).crop(Positions.CENTER).asBufferedImage();
286    }
287    
288    /**
289     * Crop the image by a specified rectangular region. The returned <code>BufferedImage</code> shares the same data array as the original image.
290     * @param src the source image.
291     * @param x The X coordinate of the upper-left corner of the specified rectangular region.
292     * @param y the Y coordinate of the upper-left corner of the specified rectangular region.
293     * @param height the width of the specified rectangular region
294     * @param width the height of the specified rectangular region
295     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
296     * @throws IOException If the source image is not readable
297     */
298    protected static BufferedImage _cropImage(BufferedImage src, int x, int y, int height, int width) throws IOException
299    {
300        return Thumbnails.of(src).scale(1).sourceRegion(x, y, width, height).asBufferedImage();
301    }
302    
303    /**
304     * Resize the buffered image
305     * @param src the source image
306     * @param height the specified height. Ignored if negative.
307     * @param width the specified width. Ignored if negative.
308     * @param maxHeight the maximum image height. Ignored if height or width is specified.
309     * @param maxWidth the maximum image width. Ignored if height or width is specified.
310     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
311     * @throws IOException If the source image is not readable
312     */
313    protected static BufferedImage _resizeImage(BufferedImage src, int height, int width, int maxHeight, int maxWidth) throws IOException
314    {
315        int srcHeight = src.getHeight();
316        int srcWidth = src.getWidth();
317        
318        int destHeight = 0;
319        int destWidth = 0;
320        
321        boolean keepAspectRatio = true;
322        
323        if (height > 0)
324        {
325            // heigth is specified
326            destHeight = height;
327            
328            if (width > 0)
329            {
330                // additionnally, width is also specified
331                destWidth = width;
332                keepAspectRatio = false;
333            }
334            else
335            {
336                // width is computed
337                destWidth = srcWidth * destHeight / srcHeight;
338            }
339        }
340        else if (width > 0)
341        {
342            // width is specified, height is computed
343            destWidth = width;
344            destHeight = srcHeight * destWidth / srcWidth;
345        }
346        else if (maxHeight > 0)
347        {
348            if (maxWidth > 0)
349            {
350                if (srcHeight <= maxHeight && srcWidth <= maxWidth)
351                {
352                    // the source image is already smaller than the destination box
353                    return src;
354                }
355                
356                destWidth = maxWidth;
357                destHeight = maxHeight;
358            }
359            else
360            {
361                if (srcHeight <= maxHeight)
362                {
363                    // the source image is already smaller than the destination box
364                    return src;
365                }
366                
367                destHeight = maxHeight;
368                destWidth = srcWidth * destHeight / srcHeight;
369            }
370        }
371        else if (maxWidth > 0)
372        {
373            if (srcWidth <= maxWidth)
374            {
375                // the source image is already smaller than the destination box
376                return src;
377            }
378            
379            destWidth = maxWidth;
380            destHeight = srcHeight * destWidth / srcWidth;
381        }
382        else
383        {
384            // No resize is required
385            return src;
386        }
387        
388        if (destHeight == srcHeight && destWidth == srcWidth)
389        {
390            // already the good format, don't change anything
391            return src;
392        }
393        
394        return Thumbnails.of(src).size(destWidth, destHeight).keepAspectRatio(keepAspectRatio).imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB).asBufferedImage();
395    }
396    
397    /**
398     * Resize the buffered image
399     * @param src the source image
400     * @param ratio ratio to resize the image (1 = same size)
401     * @return a scaled BufferedImage. If no size modification is required, this will return the src image.
402     * @throws IOException If the source image is not readable
403     */
404    protected static BufferedImage _resizeImage(BufferedImage src, double ratio) throws IOException
405    {
406        if (ratio == 1)
407        {
408            return src;
409        }
410        return Thumbnails.of(src).scale(ratio).imageType(src.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB).asBufferedImage();
411    }
412}