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