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