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