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