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