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}