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}