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