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