001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.loader; 029 030import com.alkacon.simapi.CmykJpegReader.ByteArrayImageInputStream; 031import com.alkacon.simapi.RenderSettings; 032import com.alkacon.simapi.Simapi; 033import com.alkacon.simapi.filter.GrayscaleFilter; 034import com.alkacon.simapi.filter.ShadowFilter; 035 036import org.opencms.ade.galleries.CmsPreviewService; 037import org.opencms.ade.galleries.shared.CmsPoint; 038import org.opencms.file.CmsFile; 039import org.opencms.file.CmsObject; 040import org.opencms.file.CmsProperty; 041import org.opencms.file.CmsPropertyDefinition; 042import org.opencms.file.CmsResource; 043import org.opencms.main.CmsLog; 044import org.opencms.main.OpenCms; 045import org.opencms.util.CmsStringUtil; 046 047import java.awt.Color; 048import java.awt.Dimension; 049import java.awt.Rectangle; 050import java.awt.image.BufferedImage; 051import java.io.ByteArrayOutputStream; 052import java.io.IOException; 053import java.io.OutputStream; 054import java.util.ArrayList; 055import java.util.Arrays; 056import java.util.Iterator; 057import java.util.List; 058import java.util.concurrent.Semaphore; 059import java.util.concurrent.atomic.AtomicInteger; 060 061import javax.imageio.ImageIO; 062import javax.imageio.ImageReader; 063 064import org.apache.commons.logging.Log; 065 066import jakarta.servlet.http.HttpServletRequest; 067 068/** 069 * Creates scaled images, acting as it's own parameter container.<p> 070 * 071 * @since 6.2.0 072 */ 073public class CmsImageScaler { 074 075 /** 076 * Result of a scaling operation.<p> 077 */ 078 private static class CmsImageScalingResult { 079 080 /** The image type used for encoding. */ 081 private String m_imageType; 082 083 /** Indicates if the image was changed by the scaler. */ 084 private boolean m_processed; 085 086 /** The resulting image. */ 087 private BufferedImage m_resultImage; 088 089 /** 090 * Creates a new scaling result.<p> 091 * 092 * @param resultImage the resulting image 093 * @param imageType the image type 094 * @param processed indicates if the image was changed 095 */ 096 CmsImageScalingResult(BufferedImage resultImage, String imageType, boolean processed) { 097 098 m_resultImage = resultImage; 099 m_imageType = imageType; 100 m_processed = processed; 101 } 102 103 /** 104 * Returns the image type.<p> 105 * 106 * @return the image type 107 */ 108 String getImageType() { 109 110 return m_imageType; 111 } 112 113 /** 114 * Returns the resulting image.<p> 115 * 116 * @return the resulting image 117 */ 118 BufferedImage getResultImage() { 119 120 return m_resultImage; 121 } 122 123 /** 124 * Returns if the image was changed.<p> 125 * 126 * @return <code>true</code> if the image was changed 127 */ 128 boolean isProcessed() { 129 130 return m_processed; 131 } 132 } 133 134 /** The name of the transparent color (for the background image). */ 135 public static final String COLOR_TRANSPARENT = "transparent"; 136 137 /** Default number of permits for the image scaling semaphore. */ 138 public static final int DEFAULT_CONCURRENCY = 6; 139 140 /** The name of the grayscale image filter. */ 141 public static final String FILTER_GRAYSCALE = "grayscale"; 142 143 /** The name of the shadow image filter. */ 144 public static final String FILTER_SHADOW = "shadow"; 145 146 /** The supported image filter names. */ 147 public static final List<String> FILTERS = Arrays.asList(new String[] {FILTER_GRAYSCALE, FILTER_SHADOW}); 148 149 /** The (optional) parameter used for sending the scale information of an image in the http request. */ 150 public static final String PARAM_SCALE = "__scale"; 151 152 /** The default maximum image size (width * height) to apply image blurring when down scaling (setting this to high may case "out of memory" errors). */ 153 public static final int SCALE_DEFAULT_MAX_BLUR_SIZE = 2500 * 2500; 154 155 /** The default maximum image size (width or height) to allow when up or down scaling an image using request parameters. */ 156 public static final int SCALE_DEFAULT_MAX_SIZE = 2500; 157 158 /** The scaler parameter to indicate the requested image background color (if required). */ 159 public static final String SCALE_PARAM_COLOR = "c"; 160 161 /** The scaler parameter to indicate crop height. */ 162 public static final String SCALE_PARAM_CROP_HEIGHT = "ch"; 163 164 /** The scaler parameter to indicate crop width. */ 165 public static final String SCALE_PARAM_CROP_WIDTH = "cw"; 166 167 /** The scaler parameter to indicate crop X coordinate. */ 168 public static final String SCALE_PARAM_CROP_X = "cx"; 169 170 /** The scaler parameter to indicate crop Y coordinate. */ 171 public static final String SCALE_PARAM_CROP_Y = "cy"; 172 173 /** The scaler parameter to indicate the requested image filter. */ 174 public static final String SCALE_PARAM_FILTER = "f"; 175 176 /** The scaler parameter to indicate the requested image height. */ 177 public static final String SCALE_PARAM_HEIGHT = "h"; 178 179 /** The scaler parameter to indicate the requested image position (if required). */ 180 public static final String SCALE_PARAM_POS = "p"; 181 182 /** The scaler parameter to indicate to requested image save quality in percent (if applicable, for example used with JPEG images). */ 183 public static final String SCALE_PARAM_QUALITY = "q"; 184 185 /** The scaler parameter to indicate to requested <code>{@link java.awt.RenderingHints}</code> settings. */ 186 public static final String SCALE_PARAM_RENDERMODE = "r"; 187 188 /** The scaler parameter to indicate the requested scale type. */ 189 public static final String SCALE_PARAM_TYPE = "t"; 190 191 /** The scaler parameter to indicate the requested image width. */ 192 public static final String SCALE_PARAM_WIDTH = "w"; 193 194 /** The log object for this class. */ 195 protected static final Log LOG = CmsLog.getLog(CmsImageScaler.class); 196 197 /** Semaphore used to limit the number of concurrent image scaling operations. */ 198 private static Semaphore semaphore = new Semaphore(DEFAULT_CONCURRENCY, true); 199 200 /** Atomic counter for concurrent image scaling operations. */ 201 private static final AtomicInteger concurrentRequests = new AtomicInteger(0); 202 203 /** Controls access to the semaphore. */ 204 private static final Object sync = new Object(); 205 206 /** The target background color (optional). */ 207 private Color m_color; 208 209 /** The height for image cropping. */ 210 private int m_cropHeight; 211 212 /** The width for image cropping. */ 213 private int m_cropWidth; 214 215 /** The x coordinate for image cropping. */ 216 private int m_cropX; 217 218 /** The y coordinate for image cropping. */ 219 private int m_cropY; 220 221 /** The list of image filter names (Strings) to apply. */ 222 private List<String> m_filters; 223 224 /** The focal point. */ 225 private CmsPoint m_focalPoint; 226 227 /** The target height (required). */ 228 private int m_height; 229 230 /** Indicates if this image scaler was only used to read the image properties. */ 231 private boolean m_isOriginalScaler; 232 233 /** The maximum image size (width * height) to apply image blurring when down scaling (setting this to high may case "out of memory" errors). */ 234 private int m_maxBlurSize; 235 236 /** The maximum target height (for scale type '5'). */ 237 private int m_maxHeight; 238 239 /** The maximum target width (for scale type '5'). */ 240 private int m_maxWidth; 241 242 /** The target position (optional). */ 243 private int m_position; 244 245 /** The target image save quality (if applicable, for example used with JPEG images) (optional). */ 246 private int m_quality; 247 248 /** The image processing renderings hints constant mode indicator (optional). */ 249 private int m_renderMode; 250 251 /** The final (parsed and corrected) scale parameters. */ 252 private String m_scaleParameters; 253 254 /** The target scale type (optional). */ 255 private int m_type; 256 257 /** The target width (required). */ 258 private int m_width; 259 260 /** 261 * Creates a new, empty image scaler object.<p> 262 */ 263 public CmsImageScaler() { 264 265 init(); 266 } 267 268 /** 269 * Creates a new image scaler initialized with the height and width of 270 * the given image contained in the byte array.<p> 271 * 272 * <b>Please note:</b>The image itself is not stored in the scaler, only the width and 273 * height dimensions of the image. To actually scale an image, you need to use 274 * <code>{@link #scaleImage(CmsFile)}</code>. This constructor is commonly used only 275 * to extract the image dimensions, for example when creating a String value for 276 * the <code>{@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}</code> property.<p> 277 * 278 * In case the byte array can not be decoded to an image, or in case of other errors, 279 * <code>{@link #isValid()}</code> will return <code>false</code>.<p> 280 * 281 * @param content the image to calculate the dimensions for 282 * @param rootPath the root path of the resource (for error logging) 283 */ 284 public CmsImageScaler(byte[] content, String rootPath) { 285 286 init(); 287 try { 288 Dimension dim = getImageDimensions(rootPath, content); 289 if (dim != null) { 290 m_width = dim.width; 291 m_height = dim.height; 292 } else { 293 // read the scaled image 294 BufferedImage image = Simapi.read(content); 295 m_height = image.getHeight(); 296 m_width = image.getWidth(); 297 } 298 } catch (Exception e) { 299 // nothing we can do about this, keep the original properties 300 if (LOG.isDebugEnabled()) { 301 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_EXTRACT_IMAGE_SIZE_1, rootPath), e); 302 } 303 // set height / width to default of -1 304 init(); 305 } 306 } 307 308 /** 309 * Creates a new image scaler based on the given base scaler and the given width and height.<p> 310 * 311 * @param base the base scaler to initialize the values with 312 * @param width the width to set for this scaler 313 * @param height the height to set for this scaler 314 */ 315 public CmsImageScaler(CmsImageScaler base, int width, int height) { 316 317 initValuesFrom(base); 318 setWidth(width); 319 setHeight(height); 320 } 321 322 /** 323 * Creates a new image scaler by reading the property <code>{@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}</code> 324 * from the given resource.<p> 325 * 326 * In case of any errors reading or parsing the property, 327 * <code>{@link #isValid()}</code> will return <code>false</code>.<p> 328 * 329 * @param cms the OpenCms user context to use when reading the property 330 * @param res the resource to read the property from 331 */ 332 public CmsImageScaler(CmsObject cms, CmsResource res) { 333 334 init(); 335 m_isOriginalScaler = true; 336 String sizeValue = null; 337 if ((cms != null) && (res != null)) { 338 try { 339 CmsProperty sizeProp = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, false); 340 if (!sizeProp.isNullProperty()) { 341 // parse property value using standard procedures 342 sizeValue = sizeProp.getValue(); 343 } 344 } catch (Exception e) { 345 LOG.debug(e.getMessage(), e); 346 } 347 try { 348 m_focalPoint = CmsPreviewService.readFocalPoint(cms, res); 349 } catch (Exception e) { 350 LOG.debug(e.getLocalizedMessage(), e); 351 } 352 } 353 if (CmsStringUtil.isNotEmpty(sizeValue)) { 354 parseParameters(sizeValue); 355 } 356 } 357 358 /** 359 * Creates a new image scaler based on the given HTTP request.<p> 360 * 361 * The maximum scale size is checked in order to prevent DOS attacks. 362 * Without this, it would be possible to request arbitrary huge images with a simple GET request, 363 * which would result in Out-Of-Memory errors if the image is just requested large enough.<p> 364 * 365 * The maximum blur size is checked since this operation is know to also cause memory issues 366 * with large images. If the original image is larger then this, no blur is applied before 367 * scaling down, which will result in a less optimal but still usable scale result.<p> 368 * 369 * @param request the HTTP request to read the parameters from 370 * @param maxScaleSize the maximum scale size (width or height) for the image 371 * @param maxBlurSize the maximum size of the image (width * height) to apply blur 372 */ 373 public CmsImageScaler(HttpServletRequest request, int maxScaleSize, int maxBlurSize) { 374 375 init(); 376 m_maxBlurSize = maxBlurSize; 377 String parameters = request.getParameter(CmsImageScaler.PARAM_SCALE); 378 if (CmsStringUtil.isNotEmpty(parameters)) { 379 parseParameters(parameters); 380 if (isValid()) { 381 // valid parameters, check if scale size is not too big 382 if ((getWidth() > maxScaleSize) || (getHeight() > maxScaleSize)) { 383 // scale size is too big, reset scaler 384 init(); 385 } 386 } 387 } 388 } 389 390 /** 391 * Creates an image scaler with manually set width and height. 392 * 393 * @param width the width 394 * @param height the height 395 */ 396 public CmsImageScaler(int width, int height) { 397 398 init(); 399 m_width = width; 400 m_height = height; 401 } 402 403 /** 404 * Creates a new image scaler based on the given parameter String.<p> 405 * 406 * @param parameters the scale parameters to use 407 */ 408 public CmsImageScaler(String parameters) { 409 410 init(); 411 if (CmsStringUtil.isNotEmpty(parameters)) { 412 parseParameters(parameters); 413 } 414 } 415 416 /** 417 * Calculate the width and height of a source image if scaled inside the given box.<p> 418 * 419 * @param sourceWidth the width of the source image 420 * @param sourceHeight the height of the source image 421 * @param boxWidth the width of the target box 422 * @param boxHeight the height of the target box 423 * 424 * @return the width [0] and height [1] of the source image if scaled inside the given box 425 */ 426 public static int[] calculateDimension(int sourceWidth, int sourceHeight, int boxWidth, int boxHeight) { 427 428 int[] result = new int[2]; 429 if ((sourceWidth <= boxWidth) && (sourceHeight <= boxHeight)) { 430 result[0] = sourceWidth; 431 result[1] = sourceHeight; 432 } else { 433 float scaleWidth = (float)boxWidth / (float)sourceWidth; 434 float scaleHeight = (float)boxHeight / (float)sourceHeight; 435 float scale = Math.min(scaleHeight, scaleWidth); 436 result[0] = Math.round(sourceWidth * scale); 437 result[1] = Math.round(sourceHeight * scale); 438 } 439 440 return result; 441 } 442 443 /** 444 * Gets image dimensions for given file 445 * @param path the image file path 446 * @param content the image file content 447 * @return dimensions of image 448 * @throws IOException if the file is not a known image 449 */ 450 public static Dimension getImageDimensions(String path, byte[] content) throws IOException { 451 452 String name = CmsResource.getName(path); 453 int pos = name.lastIndexOf("."); 454 if (pos == -1) { 455 LOG.warn("Couldn't determine image dimensions for " + path); 456 return null; 457 } 458 String suffix = name.substring(pos + 1); 459 Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix); 460 while (iter.hasNext()) { 461 ImageReader reader = iter.next(); 462 try { 463 ByteArrayImageInputStream stream = new ByteArrayImageInputStream(content); 464 reader.setInput(stream); 465 int minIndex = reader.getMinIndex(); 466 int width = reader.getWidth(minIndex); 467 int height = reader.getHeight(minIndex); 468 return new Dimension(width, height); 469 } catch (IOException e) { 470 LOG.warn("Problem determining image size for " + path + ": " + e.getLocalizedMessage(), e); 471 } finally { 472 reader.dispose(); 473 } 474 } 475 LOG.warn("Couldn't determine image dimensions for " + path); 476 return null; 477 } 478 479 /** 480 * Sets the image scaling concurrency level, i.e. the maximum number of threads allowed to scale images concurrently. 481 * 482 * @param numThreads the maximum number of threads that can scale images concurrently 483 */ 484 public static void setConcurrency(int numThreads) { 485 486 synchronized (sync) { 487 if (semaphore != null) { 488 semaphore.drainPermits(); 489 } 490 semaphore = new Semaphore(numThreads, true); 491 } 492 } 493 494 /** 495 * Acquires a permit from the image scaling semaphore. 496 * 497 * @throws InterruptedException if the acquire operation is interrupted 498 */ 499 private static void acquireSemaphore() throws InterruptedException { 500 501 semaphore.acquire(); 502 int requests = concurrentRequests.incrementAndGet(); 503 LOG.trace("Concurrent image scaling requests: " + requests); 504 } 505 506 /** 507 * Releases the image scaling semaphore. 508 */ 509 private static void releaseSemaphore() { 510 511 concurrentRequests.decrementAndGet(); 512 semaphore.release(); 513 } 514 515 /** 516 * Adds a filter name to the list of filters that should be applied to the image.<p> 517 * 518 * @param filter the filter name to add 519 */ 520 public void addFilter(String filter) { 521 522 if (CmsStringUtil.isNotEmpty(filter)) { 523 filter = filter.trim().toLowerCase(); 524 if (FILTERS.contains(filter)) { 525 m_filters.add(filter); 526 } 527 } 528 } 529 530 /** 531 * @see java.lang.Object#clone() 532 */ 533 @Override 534 public Object clone() { 535 536 CmsImageScaler clone = new CmsImageScaler(); 537 clone.initValuesFrom(this); 538 return clone; 539 } 540 541 /** 542 * Returns the color.<p> 543 * 544 * @return the color 545 */ 546 public Color getColor() { 547 548 return m_color; 549 } 550 551 /** 552 * Returns the color as a String.<p> 553 * 554 * @return the color as a String 555 */ 556 public String getColorString() { 557 558 StringBuffer result = new StringBuffer(); 559 if (m_color == Simapi.COLOR_TRANSPARENT) { 560 result.append(COLOR_TRANSPARENT); 561 } else { 562 if (m_color.getRed() < 16) { 563 result.append('0'); 564 } 565 result.append(Integer.toString(m_color.getRed(), 16)); 566 if (m_color.getGreen() < 16) { 567 result.append('0'); 568 } 569 result.append(Integer.toString(m_color.getGreen(), 16)); 570 if (m_color.getBlue() < 16) { 571 result.append('0'); 572 } 573 result.append(Integer.toString(m_color.getBlue(), 16)); 574 } 575 return result.toString(); 576 } 577 578 /** 579 * Returns the crop area height.<p> 580 * 581 * Use {@link #setCropArea(int, int, int, int)} to set this value.<p> 582 * 583 * @return the crop area height 584 */ 585 public int getCropHeight() { 586 587 return m_cropHeight; 588 } 589 590 /** 591 * Returns a new image scaler that is a cropped rescaler from <code>this</code> cropped scaler 592 * size to the given target scaler size.<p> 593 * 594 * @param target the image scaler that holds the target image dimensions 595 * 596 * @return a new image scaler that is a cropped rescaler from <code>this</code> cropped scaler 597 * size to the given target scaler size 598 * 599 * @see #getReScaler(CmsImageScaler) 600 * @see #setCropArea(int, int, int, int) 601 */ 602 public CmsImageScaler getCropScaler(CmsImageScaler target) { 603 604 // first re-scale the image (if required) 605 CmsImageScaler result = getReScaler(target); 606 // now use the crop area from the original 607 result.setCropArea(m_cropX, m_cropY, m_cropWidth, m_cropHeight); 608 return result; 609 } 610 611 /** 612 * Returns the crop area width.<p> 613 * 614 * Use {@link #setCropArea(int, int, int, int)} to set this value.<p> 615 * 616 * @return the crop area width 617 */ 618 public int getCropWidth() { 619 620 return m_cropWidth; 621 } 622 623 /** 624 * Returns the crop area X start coordinate.<p> 625 * 626 * Use {@link #setCropArea(int, int, int, int)} to set this value.<p> 627 * 628 * @return the crop area X start coordinate 629 */ 630 public int getCropX() { 631 632 return m_cropX; 633 } 634 635 /** 636 * Returns the crop area Y start coordinate.<p> 637 * 638 * Use {@link #setCropArea(int, int, int, int)} to set this value.<p> 639 * 640 * @return the crop area Y start coordinate 641 */ 642 public int getCropY() { 643 644 return m_cropY; 645 } 646 647 /** 648 * Returns a new image scaler that is a down scale from the size of <code>this</code> scaler 649 * to the given scaler size.<p> 650 * 651 * If no down scale from this to the given scaler is required according to 652 * {@link #isDownScaleRequired(CmsImageScaler)}, then <code>null</code> is returned.<p> 653 * 654 * @param downScaler the image scaler that holds the down scaled target image dimensions 655 * 656 * @return a new image scaler that is a down scale from the size of <code>this</code> scaler 657 * to the given target scaler size, or <code>null</code> 658 */ 659 public CmsImageScaler getDownScaler(CmsImageScaler downScaler) { 660 661 if (!isDownScaleRequired(downScaler)) { 662 // no down scaling is required 663 return null; 664 } 665 666 int downHeight = downScaler.getHeight(); 667 int downWidth = downScaler.getWidth(); 668 669 int height = getHeight(); 670 int width = getWidth(); 671 672 if (((height > width) && (downHeight < downWidth)) || ((width > height) && (downWidth < downHeight))) { 673 // adjust orientation 674 downHeight = downWidth; 675 downWidth = downScaler.getHeight(); 676 } 677 678 if (width > downWidth) { 679 // width is too large, re-calculate height 680 float scale = (float)downWidth / (float)width; 681 downHeight = Math.round(height * scale); 682 } else if (height > downHeight) { 683 // height is too large, re-calculate width 684 float scale = (float)downHeight / (float)height; 685 downWidth = Math.round(width * scale); 686 } else { 687 // something is wrong, don't down scale 688 return null; 689 } 690 691 // now create and initialize the result scaler 692 return new CmsImageScaler(downScaler, downWidth, downHeight); 693 } 694 695 /** 696 * Returns the list of image filter names (Strings) to be applied to the image.<p> 697 * 698 * @return the list of image filter names (Strings) to be applied to the image 699 */ 700 public List<String> getFilters() { 701 702 return m_filters; 703 } 704 705 /** 706 * Returns the list of image filter names (Strings) to be applied to the image as a String.<p> 707 * 708 * @return the list of image filter names (Strings) to be applied to the image as a String 709 */ 710 public String getFiltersString() { 711 712 StringBuffer result = new StringBuffer(); 713 Iterator<String> i = m_filters.iterator(); 714 while (i.hasNext()) { 715 String filter = i.next(); 716 result.append(filter); 717 if (i.hasNext()) { 718 result.append(':'); 719 } 720 } 721 return result.toString(); 722 } 723 724 /** 725 * Gets the focal point, or null if it is not set.<p> 726 * 727 * @return the focal point 728 */ 729 public CmsPoint getFocalPoint() { 730 731 return m_focalPoint; 732 } 733 734 /** 735 * Returns the height.<p> 736 * 737 * @return the height 738 */ 739 public int getHeight() { 740 741 return m_height; 742 } 743 744 /** 745 * Returns the image type from the given file name based on the file suffix (extension) 746 * and the available image writers.<p> 747 * 748 * For example, for the file name "opencms.gif" the type is GIF, for 749 * "opencms.jpg" is is "JPEG" etc.<p> 750 * 751 * In case the input filename has no suffix, or there is no known image writer for the format defined 752 * by the suffix, <code>null</code> is returned.<p> 753 * 754 * Any non-null result can be used if an image type input value is required.<p> 755 * 756 * @param filename the file name to get the type for 757 * 758 * @return the image type from the given file name based on the suffix and the available image writers, 759 * or null if no image writer is available for the format 760 */ 761 public String getImageType(String filename) { 762 763 return Simapi.getImageType(filename); 764 } 765 766 /** 767 * Returns the maximum image size (width * height) to apply image blurring when down scaling images.<p> 768 * 769 * Image blurring is required to achieve the best results for down scale operations when the target image size 770 * is 2 times or more smaller then the original image size. 771 * This parameter controls the maximum size (width * height) of an 772 * image that is blurred before it is down scaled. If the image is larger, no blurring is done. 773 * Image blurring is an expensive operation in both CPU usage and memory consumption. 774 * Setting the blur size to large may case "out of memory" errors.<p> 775 * 776 * @return the maximum image size (width * height) to apply image blurring when down scaling images 777 */ 778 public int getMaxBlurSize() { 779 780 return m_maxBlurSize; 781 } 782 783 /** 784 * Returns the maximum target height (for scale type '5').<p> 785 * 786 * @return the maximum target height (for scale type '5') 787 */ 788 public int getMaxHeight() { 789 790 return m_maxHeight; 791 } 792 793 /** 794 * Returns the maximum target width (for scale type '5').<p> 795 * 796 * @return the maximum target width (for scale type '5'). 797 */ 798 public int getMaxWidth() { 799 800 return m_maxWidth; 801 } 802 803 /** 804 * Returns the image pixel count, that is the image with multiplied by the image height.<p> 805 * 806 * If this scaler is not valid (see {@link #isValid()}) the result is undefined.<p> 807 * 808 * @return the image pixel count, that is the image with multiplied by the image height 809 */ 810 public int getPixelCount() { 811 812 return m_width * m_height; 813 } 814 815 /** 816 * Returns the position.<p> 817 * 818 * @return the position 819 */ 820 public int getPosition() { 821 822 return m_position; 823 } 824 825 /** 826 * Returns the image saving quality in percent (0 - 100).<p> 827 * 828 * This is used only if applicable, for example when saving JPEG images.<p> 829 * 830 * @return the image saving quality in percent 831 */ 832 public int getQuality() { 833 834 return m_quality; 835 } 836 837 /** 838 * Returns the image rendering mode constant.<p> 839 * 840 * Possible values are:<dl> 841 * <dt>{@link Simapi#RENDER_QUALITY} (default)</dt> 842 * <dd>Use best possible image processing - this may be slow sometimes.</dd> 843 * 844 * <dt>{@link Simapi#RENDER_SPEED}</dt> 845 * <dd>Fastest image processing but worse results - use this for thumbnails or where speed is more important then quality.</dd> 846 * 847 * <dt>{@link Simapi#RENDER_MEDIUM}</dt> 848 * <dd>Use default rendering hints from JVM - not recommended since it's almost as slow as the {@link Simapi#RENDER_QUALITY} mode.</dd></dl> 849 * 850 * @return the image rendering mode constant 851 */ 852 public int getRenderMode() { 853 854 return m_renderMode; 855 } 856 857 /** 858 * Creates a request parameter configured with the values from this image scaler, also 859 * appends a <code>'?'</code> char as a prefix so that this may be directly appended to an image URL.<p> 860 * 861 * This can be appended to an image request in order to apply image scaling parameters.<p> 862 * 863 * @return a request parameter configured with the values from this image scaler 864 * @see #toRequestParam() 865 */ 866 public String getRequestParam() { 867 868 return toRequestParam(); 869 } 870 871 /** 872 * Returns a new image scaler that is a rescaler from <code>this</code> scaler 873 * size to the given target scaler size.<p> 874 * 875 * The height of the target image is calculated in proportion 876 * to the original image width. If the width of the the original image is not known, 877 * the target image width is calculated in proportion to the original image height.<p> 878 * 879 * @param target the image scaler that holds the target image dimensions 880 * 881 * @return a new image scaler that is a rescaler from the <code>this</code> scaler 882 * size to the given target scaler size 883 */ 884 public CmsImageScaler getReScaler(CmsImageScaler target) { 885 886 int height = target.getHeight(); 887 int width = target.getWidth(); 888 int type = target.getType(); 889 890 if (type == 5) { 891 // best fit option without upscale in the provided dimensions 892 if (target.isValid()) { 893 // ensure we have sensible values for maxWidth / minWidth even if one has not been set 894 float maxWidth = target.getMaxWidth() > 0 ? target.getMaxWidth() : height; 895 float maxHeight = target.getMaxHeight() > 0 ? target.getMaxHeight() : width; 896 // calculate the factor of the image and the 3 possible target dimensions 897 float scaleOfImage = (float)getWidth() / (float)getHeight(); 898 float[] scales = new float[3]; 899 scales[0] = (float)width / (float)height; 900 scales[1] = width / maxHeight; 901 scales[2] = maxWidth / height; 902 int useScale = calculateClosest(scaleOfImage, scales); 903 int[] dimensions; 904 switch (useScale) { 905 case 1: 906 dimensions = calculateDimension(getWidth(), getHeight(), width, (int)maxHeight); 907 break; 908 case 2: 909 dimensions = calculateDimension(getWidth(), getHeight(), (int)maxWidth, height); 910 break; 911 case 0: 912 default: 913 dimensions = calculateDimension(getWidth(), getHeight(), width, height); 914 break; 915 } 916 width = dimensions[0]; 917 height = dimensions[1]; 918 } else { 919 // target not valid, switch type to 1 (no upscale) 920 type = 1; 921 } 922 } 923 924 if (type != 5) { 925 if ((width > 0) && (getWidth() > 0)) { 926 // width is known, calculate height 927 float scale = (float)width / (float)getWidth(); 928 height = Math.round(getHeight() * scale); 929 } else if ((height > 0) && (getHeight() > 0)) { 930 // height is known, calculate width 931 float scale = (float)height / (float)getHeight(); 932 width = Math.round(getWidth() * scale); 933 } else if (isValid() && !target.isValid()) { 934 // scaler is not valid but original is, so use original size of image 935 width = getWidth(); 936 height = getHeight(); 937 } 938 } 939 940 if ((type == 9) && ((width > getWidth()) || (height > getHeight()))) { 941 // type 9 with "no upscale" has been requested but target size is larger than original size 942 width = getWidth(); 943 height = getHeight(); 944 } 945 946 if ((type == 1) && (!target.isValid())) { 947 // type 1 with "no upscale" has been requested, only one target dimension was given 948 if ((target.getWidth() > 0) && (getWidth() < width)) { 949 // target width was given, target image should have this width 950 height = getHeight(); 951 } else if ((target.getHeight() > 0) && (getHeight() < height)) { 952 // target height was given, target image should have this height 953 width = getWidth(); 954 } 955 } 956 957 // now create and initialize the result scaler 958 CmsImageScaler result = new CmsImageScaler(target, width, height); 959 // type may have been switched 960 result.setType(type); 961 return result; 962 } 963 964 /** 965 * Returns the type.<p> 966 * 967 * Possible values are:<dl> 968 * 969 * <dt>0 (default): Scale to exact target size with background padding</dt><dd><ul> 970 * <li>enlarge image to fit in target size (if required) 971 * <li>reduce image to fit in target size (if required) 972 * <li>keep image aspect ratio / proportions intact 973 * <li>fill up with bgcolor to reach exact target size 974 * <li>fit full image inside target size (only applies if reduced)</ul></dd> 975 * 976 * <dt>1: Thumbnail generation mode (like 0 but no image enlargement)</dt><dd><ul> 977 * <li>dont't enlarge image 978 * <li>reduce image to fit in target size (if required) 979 * <li>keep image aspect ratio / proportions intact 980 * <li>fill up with bgcolor to reach exact target size 981 * <li>fit full image inside target size (only applies if reduced)</ul></dd> 982 * 983 * <dt>2: Scale to exact target size, crop what does not fit</dt><dd><ul> 984 * <li>enlarge image to fit in target size (if required) 985 * <li>reduce image to fit in target size (if required) 986 * <li>keep image aspect ratio / proportions intact 987 * <li>fit full image inside target size (crop what does not fit)</ul></dd> 988 * 989 * <dt>3: Scale and keep image proportions, target size variable</dt><dd><ul> 990 * <li>enlarge image to fit in target size (if required) 991 * <li>reduce image to fit in target size (if required) 992 * <li>keep image aspect ratio / proportions intact 993 * <li>scaled image will not be padded or cropped, so target size is likely not the exact requested size</ul></dd> 994 * 995 * <dt>4: Don't keep image proportions, use exact target size</dt><dd><ul> 996 * <li>enlarge image to fit in target size (if required) 997 * <li>reduce image to fit in target size (if required) 998 * <li>don't keep image aspect ratio / proportions intact 999 * <li>the image will be scaled exactly to the given target size and likely will be loose proportions</ul></dd> 1000 * </dl> 1001 * 1002 * <dt>5: Scale and keep image proportions without enlargement, target size variable with optional max width and height</dt><dd><ul> 1003 * <li>dont't enlarge image 1004 * <li>reduce image to fit in target size (if required) 1005 * <li>keep image aspect ratio / proportions intact 1006 * <li>best fit into target width / height _OR_ width / maxHeight _OR_ maxWidth / height 1007 * <li>scaled image will not be padded or cropped, so target size is likely not the exact requested size</ul></dd> 1008 * 1009 * <dt>6: Crop around point: Use exact pixels</dt><dd><ul> 1010 * <li>This type only applies for image crop operations (full crop parameters must be provided). 1011 * <li>In this case the crop coordinates <code>x, y</code> are treated as a point in the middle of <code>width, height</code>. 1012 * <li>With this type, the pixels from the source image are used 1:1 for the target image.</ul></dd> 1013 * 1014 * <dt>7: Crop around point: Use pixels for target size, get maximum out of image</dt><dd><ul> 1015 * <li>This type only applies for image crop operations (full crop parameters must be provided). 1016 * <li>In this case the crop coordinates <code>x, y</code> are treated as a point in the middle of <code>width, height</code>. 1017 * <li>With this type, as much as possible from the source image is fitted in the target image size.</ul></dd> 1018 * 1019 * <dt>8: Focal point mode.</dt> 1020 * <p>If a focal point is set on this scaler, this mode will first crop a region defined by cx,cy,cw,ch from the original 1021 * image, then select the largest region of the aspect ratio defined by w/h in the cropped image containing the focal point, and finally 1022 * scale that region to size w x h.</p> 1023 * 1024 * <dt>9: Scale and keep image proportions, target size variable, no image enlargement</dt><dd><ul> 1025 * <li>dont't enlarge image 1026 * <li>reduce image to fit in target size (if required) 1027 * <li>keep image aspect ratio / proportions intact 1028 * <li>scaled image will not be padded or cropped, so target size is likely not the exact requested size</ul></dd> 1029 * 1030 * @return the type 1031 */ 1032 public int getType() { 1033 1034 return m_type; 1035 } 1036 1037 /** 1038 * Returns the width.<p> 1039 * 1040 * @return the width 1041 */ 1042 public int getWidth() { 1043 1044 return m_width; 1045 } 1046 1047 /** 1048 * Returns a new image scaler that is a width based down scale from the size of <code>this</code> scaler 1049 * to the given scaler size.<p> 1050 * 1051 * If no down scale from this to the given scaler is required because the width of <code>this</code> 1052 * scaler is not larger than the target width, then the image dimensions of <code>this</code> scaler 1053 * are unchanged in the result scaler. No up scaling is done!<p> 1054 * 1055 * @param downScaler the image scaler that holds the down scaled target image dimensions 1056 * 1057 * @return a new image scaler that is a down scale from the size of <code>this</code> scaler 1058 * to the given target scaler size 1059 */ 1060 public CmsImageScaler getWidthScaler(CmsImageScaler downScaler) { 1061 1062 int width = downScaler.getWidth(); 1063 int height; 1064 1065 if (getWidth() > width) { 1066 // width is too large, re-calculate height 1067 float scale = (float)width / (float)getWidth(); 1068 height = Math.round(getHeight() * scale); 1069 } else { 1070 // width is ok 1071 width = getWidth(); 1072 height = getHeight(); 1073 } 1074 1075 // now create and initialize the result scaler 1076 return new CmsImageScaler(downScaler, width, height); 1077 } 1078 1079 /** 1080 * @see java.lang.Object#hashCode() 1081 */ 1082 @Override 1083 public int hashCode() { 1084 1085 return toString().hashCode(); 1086 } 1087 1088 /** 1089 * Returns <code>true</code> if all required parameters for image cropping are available.<p> 1090 * 1091 * Required parameters are <code>"cx","cy"</code> (x, y start coordinate), 1092 * and <code>"ch","cw"</code> (crop height and width).<p> 1093 * 1094 * @return <code>true</code> if all required cropping parameters are available 1095 */ 1096 public boolean isCropping() { 1097 1098 return (m_cropX >= 0) && (m_cropY >= 0) && (m_cropHeight > 0) && (m_cropWidth > 0); 1099 } 1100 1101 /** 1102 * Returns <code>true</code> if this image scaler must be down scaled when compared to the 1103 * given "down scale" image scaler.<p> 1104 * 1105 * If either <code>this</code> scaler or the given <code>downScaler</code> is invalid according to 1106 * {@link #isValid()}, then <code>false</code> is returned.<p> 1107 * 1108 * The use case: <code>this</code> scaler represents an image (that is contains width and height of 1109 * an image). The <code>downScaler</code> represents the maximum wanted image. The scalers 1110 * are compared and if the image represented by <code>this</code> scaler is too large, 1111 * <code>true</code> is returned. Image orientation is ignored, so for example an image with 600x800 pixel 1112 * will NOT be down scaled if the target size is 800x600 but kept unchanged.<p> 1113 * 1114 * @param downScaler the down scaler to compare this image scaler with 1115 * 1116 * @return <code>true</code> if this image scaler must be down scaled when compared to the 1117 * given "down scale" image scaler 1118 */ 1119 public boolean isDownScaleRequired(CmsImageScaler downScaler) { 1120 1121 if ((downScaler == null) || !isValid() || !downScaler.isValid()) { 1122 // one of the scalers is invalid 1123 return false; 1124 } 1125 1126 if (getPixelCount() < (downScaler.getPixelCount() / 2)) { 1127 // the image has much less pixels then the target, so don't downscale 1128 return false; 1129 } 1130 1131 int downWidth = downScaler.getWidth(); 1132 int downHeight = downScaler.getHeight(); 1133 if (downHeight > downWidth) { 1134 // normalize image orientation - the width should always be the large side 1135 downWidth = downHeight; 1136 downHeight = downScaler.getWidth(); 1137 } 1138 int height = getHeight(); 1139 int width = getWidth(); 1140 if (height > width) { 1141 // normalize image orientation - the width should always be the large side 1142 width = height; 1143 height = getWidth(); 1144 } 1145 1146 return (width > downWidth) || (height > downHeight); 1147 } 1148 1149 /** 1150 * Returns <code>true</code> if the image scaler was 1151 * only used to read image properties from the VFS. 1152 * 1153 * @return <code>true</code> if the image scaler was 1154 * only used to read image properties from the VFS 1155 */ 1156 public boolean isOriginalScaler() { 1157 1158 return m_isOriginalScaler; 1159 } 1160 1161 /** 1162 * Returns <code>true</code> if all required parameters are available.<p> 1163 * 1164 * Required parameters are <code>"h"</code> (height), and <code>"w"</code> (width).<p> 1165 * 1166 * @return <code>true</code> if all required parameters are available 1167 */ 1168 public boolean isValid() { 1169 1170 return (m_width > 0) && (m_height > 0); 1171 } 1172 1173 /** 1174 * Parses the given parameters and sets the internal scaler variables accordingly.<p> 1175 * 1176 * The parameter String must have a format like <code>"h:100,w:200,t:1"</code>, 1177 * that is a comma separated list of attributes followed by a colon ":", followed by a value. 1178 * As possible attributes, use the constants from this class that start with <code>SCALE_PARAM</Code> 1179 * for example {@link #SCALE_PARAM_HEIGHT} or {@link #SCALE_PARAM_WIDTH}.<p> 1180 * 1181 * @param parameters the parameters to parse 1182 */ 1183 public void parseParameters(String parameters) { 1184 1185 m_width = -1; 1186 m_height = -1; 1187 m_position = 0; 1188 m_type = 0; 1189 m_color = Simapi.COLOR_TRANSPARENT; 1190 m_cropX = -1; 1191 m_cropY = -1; 1192 m_cropWidth = -1; 1193 m_cropHeight = -1; 1194 1195 List<String> tokens = CmsStringUtil.splitAsList(parameters, ','); 1196 Iterator<String> it = tokens.iterator(); 1197 String k; 1198 String v; 1199 while (it.hasNext()) { 1200 String t = it.next(); 1201 // extract key and value 1202 k = null; 1203 v = null; 1204 int idx = t.indexOf(':'); 1205 if (idx >= 0) { 1206 k = t.substring(0, idx).trim(); 1207 if (t.length() > idx) { 1208 v = t.substring(idx + 1).trim(); 1209 } 1210 } 1211 if (CmsStringUtil.isNotEmpty(k) && CmsStringUtil.isNotEmpty(v)) { 1212 // key and value are available 1213 if (SCALE_PARAM_HEIGHT.equals(k)) { 1214 // image height 1215 m_height = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1216 } else if (SCALE_PARAM_WIDTH.equals(k)) { 1217 // image width 1218 m_width = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1219 } else if (SCALE_PARAM_CROP_X.equals(k)) { 1220 // crop x coordinate 1221 m_cropX = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1222 } else if (SCALE_PARAM_CROP_Y.equals(k)) { 1223 // crop y coordinate 1224 m_cropY = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1225 } else if (SCALE_PARAM_CROP_WIDTH.equals(k)) { 1226 // crop width 1227 m_cropWidth = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1228 } else if (SCALE_PARAM_CROP_HEIGHT.equals(k)) { 1229 // crop height 1230 m_cropHeight = CmsStringUtil.getIntValue(v, Integer.MIN_VALUE, k); 1231 } else if (SCALE_PARAM_TYPE.equals(k)) { 1232 // scaling type 1233 setType(CmsStringUtil.getIntValue(v, -1, CmsImageScaler.SCALE_PARAM_TYPE)); 1234 } else if (SCALE_PARAM_COLOR.equals(k)) { 1235 // image background color 1236 setColor(v); 1237 } else if (SCALE_PARAM_POS.equals(k)) { 1238 // image position (depends on scale type) 1239 setPosition(CmsStringUtil.getIntValue(v, -1, CmsImageScaler.SCALE_PARAM_POS)); 1240 } else if (SCALE_PARAM_QUALITY.equals(k)) { 1241 // image position (depends on scale type) 1242 setQuality(CmsStringUtil.getIntValue(v, 0, k)); 1243 } else if (SCALE_PARAM_RENDERMODE.equals(k)) { 1244 // image position (depends on scale type) 1245 setRenderMode(CmsStringUtil.getIntValue(v, 0, k)); 1246 } else if (SCALE_PARAM_FILTER.equals(k)) { 1247 // image filters to apply 1248 setFilters(v); 1249 } else { 1250 if (LOG.isDebugEnabled()) { 1251 LOG.debug(Messages.get().getBundle().key(Messages.ERR_INVALID_IMAGE_SCALE_PARAMS_2, k, v)); 1252 } 1253 } 1254 } else { 1255 if (LOG.isDebugEnabled()) { 1256 LOG.debug(Messages.get().getBundle().key(Messages.ERR_INVALID_IMAGE_SCALE_PARAMS_2, k, v)); 1257 } 1258 } 1259 } 1260 // initialize image crop area 1261 initCropArea(); 1262 } 1263 1264 /** 1265 * Returns a scaled version of the given image byte content according this image scalers parameters.<p> 1266 * 1267 * @param content the image byte content to scale 1268 * @param image if this is set, this image will be used as base for the scaling rather than a new image read from the content byte array 1269 * @param rootPath the root path of the image file in the VFS 1270 * 1271 * @return a scaled version of the given image byte content according to the provided scaler parameters 1272 */ 1273 public byte[] scaleImage(byte[] content, BufferedImage image, String rootPath) { 1274 1275 ByteArrayOutputStream out = new ByteArrayOutputStream(content.length); 1276 scaleImageTo(content, image, rootPath, out); 1277 return out.toByteArray(); 1278 } 1279 1280 /** 1281 * Returns a scaled version of the given image byte content according this image scalers parameters.<p> 1282 * 1283 * @param content the image byte content to scale 1284 * @param rootPath the root path of the image file in the VFS 1285 * 1286 * @return a scaled version of the given image byte content according to the provided scaler parameters 1287 */ 1288 public byte[] scaleImage(byte[] content, String rootPath) { 1289 1290 return scaleImage(content, (BufferedImage)null, rootPath); 1291 } 1292 1293 /** 1294 * Returns a scaled version of the given image file according this image scalers parameters.<p> 1295 * 1296 * @param file the image file to scale 1297 * 1298 * @return a scaled version of the given image file according to the provided scaler parameters 1299 */ 1300 public byte[] scaleImage(CmsFile file) { 1301 1302 return scaleImage(file.getContents(), file.getRootPath()); 1303 } 1304 1305 /** 1306 * Writes a scaled version of the given image byte content according this image scalers parameters to an output 1307 * stream.<p> 1308 * 1309 * @param content the image byte content to scale 1310 * @param image if this is set, this image will be used as base for the scaling rather than a new image read from the content byte array 1311 * @param rootPath the root path of the image file in the VFS 1312 * @param out the output stream to write the scaled image to 1313 */ 1314 public void scaleImageTo(byte[] content, BufferedImage image, String rootPath, OutputStream out) { 1315 1316 try { 1317 acquireSemaphore(); 1318 try { 1319 CmsImageScalingResult result = processImage(content, image, rootPath); 1320 if ((result == null) || !result.isProcessed()) { 1321 out.write(content); 1322 } else { 1323 createSimapi(createRenderSettings()).write(result.getResultImage(), out, result.getImageType()); 1324 } 1325 } finally { 1326 releaseSemaphore(); 1327 } 1328 } catch (InterruptedException e) { 1329 LOG.warn("Waiting on image scaling semaphore was interrupted", e); 1330 try { 1331 out.write(content); 1332 } catch (IOException ioException) { 1333 LOG.warn("Writing original image after interrupted scaling failed", ioException); 1334 } 1335 } catch (IOException e) { 1336 LOG.warn("Writing scaled image failed", e); 1337 } 1338 } 1339 1340 /** 1341 * Sets the color.<p> 1342 * 1343 * @param color the color to set 1344 */ 1345 public void setColor(Color color) { 1346 1347 m_color = color; 1348 } 1349 1350 /** 1351 * Sets the color as a String.<p> 1352 * 1353 * @param value the color to set 1354 */ 1355 public void setColor(String value) { 1356 1357 if (COLOR_TRANSPARENT.indexOf(value) == 0) { 1358 setColor(Simapi.COLOR_TRANSPARENT); 1359 } else { 1360 setColor(CmsStringUtil.getColorValue(value, Simapi.COLOR_TRANSPARENT, SCALE_PARAM_COLOR)); 1361 } 1362 } 1363 1364 /** 1365 * Sets the image crop area.<p> 1366 * 1367 * @param x the x coordinate for the crop 1368 * @param y the y coordinate for the crop 1369 * @param width the crop width 1370 * @param height the crop height 1371 */ 1372 public void setCropArea(int x, int y, int width, int height) { 1373 1374 m_cropX = x; 1375 m_cropY = y; 1376 m_cropWidth = width; 1377 m_cropHeight = height; 1378 } 1379 1380 /** 1381 * Sets the list of filters as a String.<p> 1382 * 1383 * @param value the list of filters to set 1384 */ 1385 public void setFilters(String value) { 1386 1387 m_filters = new ArrayList<String>(); 1388 List<String> filters = CmsStringUtil.splitAsList(value, ':'); 1389 Iterator<String> i = filters.iterator(); 1390 while (i.hasNext()) { 1391 String filter = i.next(); 1392 filter = filter.trim().toLowerCase(); 1393 Iterator<String> j = FILTERS.iterator(); 1394 while (j.hasNext()) { 1395 String candidate = j.next(); 1396 if (candidate.startsWith(filter)) { 1397 // found a matching filter 1398 addFilter(candidate); 1399 break; 1400 } 1401 } 1402 } 1403 } 1404 1405 /** 1406 * Sets the focal point.<p> 1407 * 1408 * @param point the new value for the focal point 1409 */ 1410 public void setFocalPoint(CmsPoint point) { 1411 1412 m_focalPoint = point; 1413 } 1414 1415 /** 1416 * Sets the height.<p> 1417 * 1418 * @param height the height to set 1419 */ 1420 public void setHeight(int height) { 1421 1422 m_height = height; 1423 } 1424 1425 /** 1426 * Sets the maximum image size (width * height) to apply image blurring when downscaling images.<p> 1427 * 1428 * @param maxBlurSize the maximum image blur size to set 1429 * 1430 * @see #getMaxBlurSize() for a more detailed description about this parameter 1431 */ 1432 public void setMaxBlurSize(int maxBlurSize) { 1433 1434 m_maxBlurSize = maxBlurSize; 1435 } 1436 1437 /** 1438 * Sets the maximum target height (for scale type '5').<p> 1439 * 1440 * @param maxHeight the maximum target height to set 1441 */ 1442 public void setMaxHeight(int maxHeight) { 1443 1444 m_maxHeight = maxHeight; 1445 } 1446 1447 /** 1448 * Sets the maximum target width (for scale type '5').<p> 1449 * 1450 * @param maxWidth the maximum target width to set 1451 */ 1452 public void setMaxWidth(int maxWidth) { 1453 1454 m_maxWidth = maxWidth; 1455 } 1456 1457 /** 1458 * Sets the scale position.<p> 1459 * 1460 * @param position the position to set 1461 */ 1462 public void setPosition(int position) { 1463 1464 switch (position) { 1465 case Simapi.POS_DOWN_LEFT: 1466 case Simapi.POS_DOWN_RIGHT: 1467 case Simapi.POS_STRAIGHT_DOWN: 1468 case Simapi.POS_STRAIGHT_LEFT: 1469 case Simapi.POS_STRAIGHT_RIGHT: 1470 case Simapi.POS_STRAIGHT_UP: 1471 case Simapi.POS_UP_LEFT: 1472 case Simapi.POS_UP_RIGHT: 1473 // position is fine 1474 m_position = position; 1475 break; 1476 default: 1477 m_position = Simapi.POS_CENTER; 1478 } 1479 } 1480 1481 /** 1482 * Sets the image saving quality in percent.<p> 1483 * 1484 * @param quality the image saving quality (in percent) to set 1485 */ 1486 public void setQuality(int quality) { 1487 1488 if (quality < 0) { 1489 m_quality = 0; 1490 } else if (quality > 100) { 1491 m_quality = 100; 1492 } else { 1493 m_quality = quality; 1494 } 1495 } 1496 1497 /** 1498 * Sets the image rendering mode constant.<p> 1499 * 1500 * @param renderMode the image rendering mode to set 1501 * 1502 * @see #getRenderMode() for a list of allowed values for the rendering mode 1503 */ 1504 public void setRenderMode(int renderMode) { 1505 1506 if ((renderMode < Simapi.RENDER_QUALITY) || (renderMode > Simapi.RENDER_SPEED)) { 1507 renderMode = Simapi.RENDER_QUALITY; 1508 } 1509 m_renderMode = renderMode; 1510 } 1511 1512 /** 1513 * Sets the scale type.<p> 1514 * 1515 * @param type the scale type to set 1516 * 1517 * @see #getType() for a detailed description of the possible values for the type 1518 */ 1519 public void setType(int type) { 1520 1521 if ((type < 0) || (type > 9)) { 1522 // invalid type, use 0 1523 m_type = 0; 1524 } else { 1525 m_type = type; 1526 } 1527 } 1528 1529 /** 1530 * Sets the width.<p> 1531 * 1532 * @param width the width to set 1533 */ 1534 public void setWidth(int width) { 1535 1536 m_width = width; 1537 } 1538 1539 /** 1540 * Creates a request parameter configured with the values from this image scaler, also 1541 * appends a <code>'?'</code> char as a prefix so that this may be directly appended to an image URL.<p> 1542 * 1543 * This can be appended to an image request in order to apply image scaling parameters.<p> 1544 * 1545 * @return a request parameter configured with the values from this image scaler 1546 */ 1547 public String toRequestParam() { 1548 1549 StringBuffer result = new StringBuffer(128); 1550 result.append('?'); 1551 result.append(PARAM_SCALE); 1552 result.append('='); 1553 result.append(toString()); 1554 1555 return result.toString(); 1556 } 1557 1558 /** 1559 * @see java.lang.Object#toString() 1560 */ 1561 @Override 1562 public String toString() { 1563 1564 if (m_scaleParameters != null) { 1565 return m_scaleParameters; 1566 } 1567 1568 StringBuffer result = new StringBuffer(64); 1569 if (isCropping()) { 1570 result.append(CmsImageScaler.SCALE_PARAM_CROP_X); 1571 result.append(':'); 1572 result.append(m_cropX); 1573 result.append(','); 1574 result.append(CmsImageScaler.SCALE_PARAM_CROP_Y); 1575 result.append(':'); 1576 result.append(m_cropY); 1577 result.append(','); 1578 result.append(CmsImageScaler.SCALE_PARAM_CROP_WIDTH); 1579 result.append(':'); 1580 result.append(m_cropWidth); 1581 result.append(','); 1582 result.append(CmsImageScaler.SCALE_PARAM_CROP_HEIGHT); 1583 result.append(':'); 1584 result.append(m_cropHeight); 1585 } 1586 if (!isCropping() || ((m_width != m_cropWidth) || (m_height != m_cropHeight))) { 1587 if (isCropping()) { 1588 result.append(','); 1589 } 1590 result.append(CmsImageScaler.SCALE_PARAM_WIDTH); 1591 result.append(':'); 1592 result.append(m_width); 1593 result.append(','); 1594 result.append(CmsImageScaler.SCALE_PARAM_HEIGHT); 1595 result.append(':'); 1596 result.append(m_height); 1597 } 1598 if (m_type > 0) { 1599 result.append(','); 1600 result.append(CmsImageScaler.SCALE_PARAM_TYPE); 1601 result.append(':'); 1602 result.append(m_type); 1603 } 1604 if (m_position > 0) { 1605 result.append(','); 1606 result.append(CmsImageScaler.SCALE_PARAM_POS); 1607 result.append(':'); 1608 result.append(m_position); 1609 } 1610 if (m_color != Color.WHITE) { 1611 result.append(','); 1612 result.append(CmsImageScaler.SCALE_PARAM_COLOR); 1613 result.append(':'); 1614 result.append(getColorString()); 1615 } 1616 if (m_quality > 0) { 1617 result.append(','); 1618 result.append(CmsImageScaler.SCALE_PARAM_QUALITY); 1619 result.append(':'); 1620 result.append(m_quality); 1621 } 1622 if (m_renderMode > 0) { 1623 result.append(','); 1624 result.append(CmsImageScaler.SCALE_PARAM_RENDERMODE); 1625 result.append(':'); 1626 result.append(m_renderMode); 1627 } 1628 if (!m_filters.isEmpty()) { 1629 result.append(','); 1630 result.append(CmsImageScaler.SCALE_PARAM_FILTER); 1631 result.append(':'); 1632 result.append(getFiltersString()); 1633 } 1634 m_scaleParameters = result.toString(); 1635 return m_scaleParameters; 1636 } 1637 1638 /** 1639 * Calculate the closest match of the given base float with the list of others.<p> 1640 * 1641 * @param base the base float to compare the other with 1642 * @param others the list of floats to compate to the base 1643 * 1644 * @return the array index of the closest match 1645 */ 1646 private int calculateClosest(float base, float[] others) { 1647 1648 int result = -1; 1649 float bestMatch = Float.MAX_VALUE; 1650 for (int count = 0; count < others.length; count++) { 1651 float difference = Math.abs(base - others[count]); 1652 if (difference < bestMatch) { 1653 // new best match found 1654 bestMatch = difference; 1655 result = count; 1656 } 1657 if (bestMatch == 0f) { 1658 // it does not get better then this 1659 break; 1660 } 1661 } 1662 return result; 1663 } 1664 1665 /** 1666 * Creates render settings for the current scaler parameters.<p> 1667 * 1668 * @return the render settings 1669 */ 1670 private RenderSettings createRenderSettings() { 1671 1672 RenderSettings renderSettings; 1673 if ((m_renderMode == 0) && (m_quality == 0)) { 1674 // use default render mode and quality 1675 renderSettings = new RenderSettings(Simapi.RENDER_QUALITY); 1676 } else { 1677 // use special render mode and/or quality 1678 renderSettings = new RenderSettings(m_renderMode); 1679 if (m_quality != 0) { 1680 renderSettings.setCompressionQuality(m_quality / 100f); 1681 } 1682 } 1683 // set max blur size 1684 renderSettings.setMaximumBlurSize(m_maxBlurSize); 1685 return renderSettings; 1686 } 1687 1688 /** 1689 * Creates a Simapi instance for the given render settings.<p> 1690 * 1691 * @param renderSettings the render settings 1692 * 1693 * @return the Simapi instance 1694 */ 1695 private Simapi createSimapi(RenderSettings renderSettings) { 1696 1697 return new Simapi(renderSettings); 1698 } 1699 1700 private Dimension getDimensionsWithSimapi(byte[] content) throws Exception { 1701 1702 BufferedImage image = Simapi.read(content); 1703 return new Dimension(image.getWidth(), image.getHeight()); 1704 } 1705 1706 /** 1707 * Initializes the members with the default values.<p> 1708 */ 1709 private void init() { 1710 1711 m_height = -1; 1712 m_width = -1; 1713 m_maxHeight = -1; 1714 m_maxWidth = -1; 1715 m_type = 0; 1716 m_position = 0; 1717 m_renderMode = 0; 1718 m_quality = 0; 1719 m_cropX = -1; 1720 m_cropY = -1; 1721 m_cropHeight = -1; 1722 m_cropWidth = -1; 1723 m_color = Color.WHITE; 1724 m_filters = new ArrayList<String>(); 1725 m_maxBlurSize = CmsImageLoader.getMaxBlurSize(); 1726 m_isOriginalScaler = false; 1727 } 1728 1729 /** 1730 * Initializes the crop area setting.<p> 1731 * 1732 * Only if all 4 required parameters have been set, the crop area is set accordingly. 1733 * Moreover, it is not required to specify the target image width and height when using crop, 1734 * because these parameters can be calculated from the crop area.<p> 1735 * 1736 * Scale type 6 and 7 are used for a 'crop around point' operation, see {@link #getType()} for a description.<p> 1737 */ 1738 private void initCropArea() { 1739 1740 if (isCropping()) { 1741 // crop area is set up correctly 1742 // adjust target image height or width if required 1743 if (m_width < 0) { 1744 m_width = m_cropWidth; 1745 } 1746 if (m_height < 0) { 1747 m_height = m_cropHeight; 1748 } 1749 if ((getType() != 6) && (getType() != 7) && (getType() != 8)) { 1750 // cropping type can only be 6 or 7 (point cropping) 1751 // all other values with cropping coordinates are invalid 1752 setType(0); 1753 } 1754 } 1755 } 1756 1757 /** 1758 * Copies all values from the given scaler into this scaler.<p> 1759 * 1760 * @param source the source scaler 1761 */ 1762 private void initValuesFrom(CmsImageScaler source) { 1763 1764 m_color = source.m_color; 1765 m_cropHeight = source.m_cropHeight; 1766 m_cropWidth = source.m_cropWidth; 1767 m_cropX = source.m_cropX; 1768 m_cropY = source.m_cropY; 1769 m_filters = new ArrayList<String>(source.m_filters); 1770 m_focalPoint = source.m_focalPoint; 1771 m_height = source.m_height; 1772 m_isOriginalScaler = source.m_isOriginalScaler; 1773 m_maxBlurSize = source.m_maxBlurSize; 1774 m_position = source.m_position; 1775 m_quality = source.m_quality; 1776 m_renderMode = source.m_renderMode; 1777 m_type = source.m_type; 1778 m_width = source.m_width; 1779 1780 } 1781 1782 /** 1783 * Processes an image and returns the scaled image result.<p> 1784 * 1785 * @param content the source image bytes, only used if no image is provided 1786 * @param image the image to scale 1787 * @param rootPath the root path of the image file in the VFS 1788 * 1789 * @return the scaling result, or <code>null</code> if the image can not be scaled 1790 */ 1791 private CmsImageScalingResult processImage(byte[] content, BufferedImage image, String rootPath) { 1792 1793 // flag for processed image 1794 boolean imageProcessed = false; 1795 // initialize image crop area 1796 initCropArea(); 1797 1798 RenderSettings renderSettings = createRenderSettings(); 1799 Simapi scaler = createSimapi(renderSettings); 1800 // calculate a valid image type supported by the imaging library (e.g. "JPEG", "GIF") 1801 String imageType = Simapi.getImageType(rootPath); 1802 if (imageType == null) { 1803 // no type given, maybe the name got mixed up 1804 String mimeType = OpenCms.getResourceManager() != null 1805 ? OpenCms.getResourceManager().getMimeType(rootPath, null, null) 1806 : null; 1807 // check if this is another known MIME type, if so DONT use it (images should not be named *.pdf) 1808 if (mimeType == null) { 1809 // no MIME type found, use JPEG format to write images to the cache 1810 imageType = Simapi.TYPE_JPEG; 1811 } 1812 } 1813 if (imageType == null) { 1814 // unknown type, unable to scale the image 1815 if (LOG.isDebugEnabled()) { 1816 LOG.debug(Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_SCALE_IMAGE_2, rootPath, toString())); 1817 } 1818 return null; 1819 } 1820 try { 1821 if (image == null) { 1822 image = Simapi.read(content); 1823 } 1824 if (image == null) { 1825 return null; 1826 } 1827 1828 if (isCropping()) { 1829 // check if the crop width / height are not larger then the source image 1830 if ((getType() == 0) && ((m_cropHeight > image.getHeight()) || (m_cropWidth > image.getWidth()))) { 1831 // crop height / width is outside of image - return image unchanged 1832 return new CmsImageScalingResult(image, imageType, false); 1833 } 1834 } 1835 1836 Color color = getColor(); 1837 1838 if (!m_filters.isEmpty()) { 1839 Iterator<String> i = m_filters.iterator(); 1840 while (i.hasNext()) { 1841 String filter = i.next(); 1842 if (FILTER_GRAYSCALE.equals(filter)) { 1843 // add a gray scale filter 1844 GrayscaleFilter grayscaleFilter = new GrayscaleFilter(); 1845 renderSettings.addImageFilter(grayscaleFilter); 1846 } else if (FILTER_SHADOW.equals(filter)) { 1847 // add a drop shadow filter 1848 ShadowFilter shadowFilter = new ShadowFilter(); 1849 shadowFilter.setXOffset(5); 1850 shadowFilter.setYOffset(5); 1851 shadowFilter.setOpacity(192); 1852 shadowFilter.setBackgroundColor(color.getRGB()); 1853 color = Simapi.COLOR_TRANSPARENT; 1854 renderSettings.setTransparentReplaceColor(Simapi.COLOR_TRANSPARENT); 1855 renderSettings.addImageFilter(shadowFilter); 1856 } 1857 } 1858 } 1859 1860 if (isCropping()) { 1861 if ((getType() == 8) && (m_focalPoint != null)) { 1862 image = scaler.cropToSize( 1863 image, 1864 m_cropX, 1865 m_cropY, 1866 m_cropWidth, 1867 m_cropHeight, 1868 m_cropWidth, 1869 m_cropHeight, 1870 color); 1871 // Find the biggest scaling factor which, when applied to a rectangle of dimensions m_width x m_height, 1872 // would allow the resulting rectangle to still fit inside a rectangle of dimensions m_cropWidth x m_cropHeight 1873 // (we have to take the minimum because a rectangle that fits on the x axis might still be out of bounds on the y axis, and 1874 // vice versa). 1875 double scaling = Math.min((1.0 * m_cropWidth) / m_width, (1.0 * m_cropHeight) / m_height); 1876 int relW = (int)(scaling * m_width); 1877 int relH = (int)(scaling * m_height); 1878 // the focal point's coordinates are in the uncropped image's coordinate system, so we have to subtract cx/cy 1879 int relX = (int)(m_focalPoint.getX() - m_cropX); 1880 int relY = (int)(m_focalPoint.getY() - m_cropY); 1881 image = scaler.cropPointToSize(image, relX, relY, false, relW, relH); 1882 if ((m_width != relW) || (m_height != relH)) { 1883 image = scaler.scale(image, m_width, m_height); 1884 } 1885 } else if ((getType() == 6) || (getType() == 7)) { 1886 // image crop operation around point 1887 image = scaler.cropPointToSize(image, m_cropX, m_cropY, getType() == 6, m_cropWidth, m_cropHeight); 1888 } else { 1889 // image crop operation 1890 image = scaler.cropToSize( 1891 image, 1892 m_cropX, 1893 m_cropY, 1894 m_cropWidth, 1895 m_cropHeight, 1896 getWidth(), 1897 getHeight(), 1898 color); 1899 } 1900 1901 imageProcessed = true; 1902 } else { 1903 // only rescale the image, if the width and height are different to the target size 1904 int imageWidth = image.getWidth(); 1905 int imageHeight = image.getHeight(); 1906 1907 // image rescale operation 1908 switch (getType()) { 1909 // select the "right" method of scaling according to the "t" parameter 1910 case 1: 1911 // thumbnail generation mode (like 0 but no image enlargement) 1912 image = scaler.resize(image, getWidth(), getHeight(), color, getPosition(), false); 1913 imageProcessed = true; 1914 break; 1915 case 2: 1916 // scale to exact target size, crop what does not fit 1917 if (((imageWidth != getWidth()) || (imageHeight != getHeight()))) { 1918 image = scaler.resize(image, getWidth(), getHeight(), getPosition()); 1919 imageProcessed = true; 1920 } 1921 break; 1922 case 3: 1923 // scale and keep image proportions, target size variable 1924 if (((imageWidth != getWidth()) || (imageHeight != getHeight()))) { 1925 image = scaler.resize(image, getWidth(), getHeight(), true); 1926 imageProcessed = true; 1927 } 1928 break; 1929 case 4: 1930 // don't keep image proportions, use exact target size 1931 if (((imageWidth != getWidth()) || (imageHeight != getHeight()))) { 1932 image = scaler.resize(image, getWidth(), getHeight(), false); 1933 imageProcessed = true; 1934 } 1935 break; 1936 case 5: 1937 // scale and keep image proportions, target size variable, include maxWidth / maxHeight option 1938 // image proportions have already been calculated so should not be a problem, use 1939 // 'false' to make sure image size exactly matches height and width attributes of generated tag 1940 if (((imageWidth != getWidth()) || (imageHeight != getHeight()))) { 1941 image = scaler.resize(image, getWidth(), getHeight(), false); 1942 imageProcessed = true; 1943 } 1944 break; 1945 case 9: 1946 // scale and keep image proportions, target size variable, no image enlargement 1947 if ((imageWidth > getWidth()) && (imageHeight > getHeight())) { 1948 image = scaler.resize(image, getWidth(), getHeight(), true); 1949 imageProcessed = true; 1950 } 1951 break; 1952 default: 1953 // scale to exact target size with background padding 1954 image = scaler.resize(image, getWidth(), getHeight(), color, getPosition(), true); 1955 imageProcessed = true; 1956 } 1957 1958 } 1959 1960 if (!m_filters.isEmpty()) { 1961 Rectangle targetSize = scaler.applyFilterDimensions(getWidth(), getHeight()); 1962 image = scaler.resize( 1963 image, 1964 (int)targetSize.getWidth(), 1965 (int)targetSize.getHeight(), 1966 Simapi.COLOR_TRANSPARENT, 1967 Simapi.POS_CENTER); 1968 image = scaler.applyFilters(image); 1969 imageProcessed = true; 1970 } 1971 1972 // get the byte result for the scaled image if some changes have been made. 1973 // otherwiese use the original image 1974 if (imageProcessed) { 1975 return new CmsImageScalingResult(image, imageType, true); 1976 } 1977 } catch (Exception e) { 1978 if (LOG.isDebugEnabled()) { 1979 LOG.debug( 1980 Messages.get().getBundle().key(Messages.ERR_UNABLE_TO_SCALE_IMAGE_2, rootPath, toString()), 1981 e); 1982 } 1983 } 1984 return new CmsImageScalingResult(image, imageType, false); 1985 } 1986}