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