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