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}