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