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