001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.util;
029
030import org.opencms.ade.galleries.shared.CmsPoint;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.jsp.CmsJspResourceWrapper;
034import org.opencms.loader.CmsImageScaler;
035import org.opencms.main.CmsException;
036import org.opencms.main.CmsLog;
037import org.opencms.main.OpenCms;
038import org.opencms.util.CmsCollectionsGenericWrapper;
039import org.opencms.util.CmsRequestUtil;
040import org.opencms.util.CmsUriSplitter;
041
042import java.util.Map;
043import java.util.TreeMap;
044
045import org.apache.commons.collections.Transformer;
046import org.apache.commons.logging.Log;
047
048/**
049 * Bean containing image information for the use in JSP (for example formatters).
050 */
051public class CmsJspImageBean {
052
053    /**
054     * Provides a Map to access hi-DPI versions of the current image.<p>
055     */
056    public class CmsScaleHiDpiTransformer implements Transformer {
057
058        /**
059         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
060         */
061        @Override
062        public Object transform(Object input) {
063
064            return createHiDpiVariation(String.valueOf(input));
065        }
066    }
067
068    /**
069     * Provides a Map to access ratio scaled versions of the current image.<p>
070     */
071    public class CmsScaleRatioTransformer implements Transformer {
072
073        /**
074         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
075         */
076        @Override
077        public Object transform(Object input) {
078
079            return createRatioVariation(String.valueOf(input));
080        }
081    }
082
083    /**
084     * Provides a Map to access width scaled versions of the current image.<p>
085     */
086    public class CmsScaleWidthTransformer implements Transformer {
087
088        /**
089         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
090         */
091        @Override
092        public Object transform(Object input) {
093
094            return createWidthVariation(String.valueOf(input));
095        }
096    }
097
098    /** The minimum dimension (width and height) a generated image must have. */
099    public static int MIN_DIMENSION = 4;
100
101    /** The log object for this class. */
102    static final Log LOG = CmsLog.getLog(CmsJspImageBean.class);
103
104    /** Size variations for source sets. */
105    static final double[] m_sizeVariants = {1.000, 0.7500, 0.5000, 0.3750, 0.2500, 0.1250};
106
107    /** The wrapped VFS resource for this image. */
108    CmsJspResourceWrapper m_resource = null;
109
110    /** Lazy initialized map of ratio scaled versions of this image. */
111    Map<String, CmsJspImageBean> m_scaleRatio = null;
112
113    /** Lazy initialized map of width scaled versions of this image. */
114    Map<String, CmsJspImageBean> m_scaleWidth = null;
115
116    /** Map used for creating a image source set. */
117    TreeMap<Integer, CmsJspImageBean> m_srcSet = null;
118
119    /** The CmsImageScaler that describes the basic adjustments (usually cropping) that have been set on the original image. */
120    private CmsImageScaler m_baseScaler;
121
122    /** The current OpenCms user context. */
123    private CmsObject m_cms = null;
124
125    /** The CmsImageScaler that is used to create a scaled version of this image. */
126    private CmsImageScaler m_currentScaler;
127
128    /**
129     * Map used to store hi-DPI variants of the image.
130     * <ul>
131     *   <li>key: the variant multiplier, e.g. "2x" (the common retina multiplier)</li>
132     *   <li>value: a CmsJspImageBean representing the hi-DPI variant</li>
133     * </ul>
134     */
135    private Map<String, CmsJspImageBean> m_hiDpiImages = null;
136
137    /** The CmsImageScaler that describes the original pixel proportions of this image. */
138    private CmsImageScaler m_originalScaler;
139
140    /** The image quality (for JPEG calculation). */
141    private int m_quality = 0;
142
143    /** The image VFS path. */
144    private String m_vfsUri;
145
146    /** The ratio of the image, width to height, for example '4-3' or '16-9'. */
147    private String m_ratio;
148
149    /** The height percentage of the image relative to the image width. */
150    private String m_ratioHeightPercentage;
151
152    /**
153     * Initializes a new image bean based on a VFS resource and optional scaler parameters.<p>
154     *
155     * @param cms the current OpenCms user context
156     * @param imageRes the VFS resource to read the image from
157     * @param scaleParams optional scaler parameters to apply to the VFS resource
158     */
159    public CmsJspImageBean(CmsObject cms, CmsResource imageRes, String scaleParams) {
160
161        init(cms, imageRes, scaleParams);
162    }
163
164    /**
165     * Initializes a new image bean based on a string pointing to a VFS resource that may contain appended scaling parameters.<p>
166     *
167     * @param cms the current OpenCms user context
168     * @param imageUri the URI to read the image from in the OpenCms VFS, may also contain appended scaling parameters
169     *
170     * @throws CmsException in case of problems reading the image from the VFS
171     */
172    public CmsJspImageBean(CmsObject cms, String imageUri)
173    throws CmsException {
174
175        setCmsObject(cms);
176        // split the given image URI to see if there are scaling parameters attached
177        CmsUriSplitter splitSrc = new CmsUriSplitter(imageUri);
178        String scaleParam = null;
179        if (splitSrc.getQuery() != null) {
180            // check if the original URI already has parameters, this is true if original has been cropped
181            String[] scaleStr = CmsRequestUtil.createParameterMap(splitSrc.getQuery()).get(CmsImageScaler.PARAM_SCALE);
182            if (scaleStr != null) {
183                scaleParam = scaleStr[0];
184            }
185        }
186
187        init(cms, cms.readResource(splitSrc.getPrefix()), scaleParam);
188    }
189
190    /**
191     * Initializes a new image bean based on a VFS input string and applies additional re-scaling.<p>
192     *
193     * The input string is is used to read the images from the VFS.
194     * It can contain scaling parameters.
195     * The additional re-scaling is then applied to the image that has been read.<p>
196     *
197     * @param cms the current uses OpenCms context
198     * @param imageUri the URI to read the image from in the OpenCms VFS, may also contain scaling parameters
199     * @param initScaler the additional re-scaling to apply to the image
200     *
201     * @throws CmsException in case of problems reading the image from the VFS
202     */
203    public CmsJspImageBean(CmsObject cms, String imageUri, CmsImageScaler initScaler)
204    throws CmsException {
205
206        this(cms, imageUri);
207
208        CmsImageScaler targetScaler = createVariation(
209            getWidth(),
210            getHeight(),
211            getBaseScaler(),
212            initScaler.getWidth(),
213            initScaler.getHeight(),
214            getQuality());
215
216        if ((targetScaler != null) && targetScaler.isValid()) {
217            setScaler(targetScaler);
218        }
219    }
220
221    /**
222     * Initializes a new empty image bean.<p>
223     *
224     * All values must be set with setters later.<p>
225     */
226    protected CmsJspImageBean() {
227
228        // all values must be set with setters later
229    }
230
231    /**
232     * Create a variation scaler fir this image.<p>
233     *
234     * @param originalWidth the original image pixel width
235     * @param originalHeight the original image pixel height
236     * @param baseScaler the base scaler that may contain crop parameters
237     * @param targetWidth the target image pixel width
238     * @param targetHeight the target image pixel height
239     * @param quality the compression quality factor to use for image generation
240     *
241     * @return the created variation scaler for this image
242     */
243    protected static CmsImageScaler createVariation(
244        int originalWidth,
245        int originalHeight,
246        CmsImageScaler baseScaler,
247        int targetWidth,
248        int targetHeight,
249        int quality) {
250
251        CmsImageScaler result = null;
252
253        if ((targetWidth <= 0) || (targetHeight <= 0)) {
254            // not all dimensions have been given, calculate the missing
255
256            double baseRatio;
257            if (baseScaler.isCropping()) {
258                // use the image crop with/height for aspect ratio calculation
259                baseRatio = (double)baseScaler.getCropWidth() / (double)baseScaler.getCropHeight();
260            } else {
261                // use the image original pixel width/height for aspect ratio calculation
262                baseRatio = (double)originalWidth / (double)originalHeight;
263            }
264
265            // one dimension is missing, calculate it from the image
266            if (targetWidth <= 0) {
267                // width is not set, calculate it with the given height and the aspect ratio
268                targetWidth = (int)Math.round(targetHeight * baseRatio);
269            } else if (targetHeight <= 0) {
270                // height is not set, calculate it with the given width and the aspect ratio
271                targetHeight = (int)Math.round(targetWidth / baseRatio);
272            }
273        }
274
275        if ((targetWidth >= MIN_DIMENSION)
276            && (targetHeight >= MIN_DIMENSION)
277            && (originalWidth >= targetWidth)
278            && (originalHeight >= targetHeight)) {
279
280            // image original dimensions are large enough, generate result scaler
281            result = new CmsImageScaler();
282
283            result.setWidth(targetWidth);
284            result.setHeight(targetHeight);
285
286            if ((baseScaler.getFocalPoint() != null)
287                && checkCropRegionContainsFocalPoint(baseScaler, baseScaler.getFocalPoint())) {
288                result.setType(8);
289                if (baseScaler.isCropping()) {
290                    result.setCropArea(
291                        baseScaler.getCropX(),
292                        baseScaler.getCropY(),
293                        baseScaler.getCropWidth(),
294                        baseScaler.getCropHeight());
295                } else {
296                    result.setCropArea(0, 0, originalWidth, originalHeight);
297                }
298            } else {
299
300                result.setType(2);
301                if (baseScaler.isCropping()) {
302
303                    double targetRatio = (double)baseScaler.getCropWidth() / (double)targetWidth;
304                    int targetCropWidth = baseScaler.getCropWidth();
305                    int targetCropHeight = (int)Math.round(targetHeight * targetRatio);
306
307                    if (targetCropHeight > baseScaler.getCropHeight()) {
308                        targetRatio = (double)baseScaler.getCropHeight() / (double)targetHeight;
309                        targetCropWidth = (int)Math.round(targetWidth * targetRatio);
310                        targetCropHeight = baseScaler.getCropHeight();
311                    }
312
313                    int targetX = baseScaler.getCropX();
314                    int targetY = baseScaler.getCropY();
315
316                    if (targetCropWidth != baseScaler.getCropWidth()) {
317                        targetX = targetX + (int)Math.round((baseScaler.getCropWidth() - targetCropWidth) / 2.0);
318                    }
319                    if (targetCropHeight != baseScaler.getCropHeight()) {
320                        targetY = targetY + (int)Math.round((baseScaler.getCropHeight() - targetCropHeight) / 2.0);
321                    }
322
323                    result.setCropArea(targetX, targetY, targetCropWidth, targetCropHeight);
324                }
325            }
326        }
327
328        if ((result != null) && (quality > 0)) {
329            // apply compression quality setting
330            result.setQuality(quality);
331        }
332        return result;
333    }
334
335    /**
336     * Helper method to check whether the focal point in the scaler is contained in the scaler's crop region.<p>
337     *
338     * If the scaler has no crop region, true is returned.
339     *
340     * @param scaler the scaler
341     * @param focalPoint the focal point to check
342     * @return true if the scaler's crop region contains the focal point
343     */
344    private static boolean checkCropRegionContainsFocalPoint(CmsImageScaler scaler, CmsPoint focalPoint) {
345
346        if (!scaler.isCropping()) {
347            return true;
348        }
349        double x = focalPoint.getX();
350        double y = focalPoint.getY();
351        return (scaler.getCropX() <= x)
352            && (x < (scaler.getCropX() + scaler.getCropWidth()))
353            && (scaler.getCropY() <= y)
354            && (y < (scaler.getCropY() + scaler.getCropHeight()));
355    }
356
357    /**
358     * adds a CmsJspImageBean as hi-DPI variant to this image
359     * @param factor the variant multiplier, e.g. "2x" (the common retina multiplier)
360     * @param image the image to be used for this variant
361     */
362    public void addHiDpiImage(String factor, CmsJspImageBean image) {
363
364        if (m_hiDpiImages == null) {
365            m_hiDpiImages = CmsCollectionsGenericWrapper.createLazyMap(new CmsScaleHiDpiTransformer());
366        }
367        m_hiDpiImages.put(factor, image);
368    }
369
370    /**
371     * Adds a number of size variations to the source set.<p>
372     *
373     * In case the screen size is not really known, it may be a good idea to add
374     * some variations for large images to make sure there are some common options in case the basic
375     * image is very large.<p>
376     *
377     * @param minWidth the minimum image width to add size variations for
378     * @param maxWidth the maximum width size variation to create
379     */
380    public void addSrcSetWidthVariants(int minWidth, int maxWidth) {
381
382        int imageWidth = getWidth();
383        if (imageWidth > minWidth) {
384            // only add variants in case the image is larger then the given minimum
385            int srcSetMaxWidth = getSrcSetMaxWidth();
386            for (double factor : m_sizeVariants) {
387                long width = Math.round(imageWidth * factor);
388                if (width > srcSetMaxWidth) {
389                    if (width <= maxWidth) {
390                        setSrcSets(createWidthVariation(String.valueOf(width)));
391                    }
392                } else {
393                    break;
394                }
395            }
396        }
397    }
398
399    /**
400     * Creates a hi-DPI scaled version of the current image.<p>
401     *
402     * @param hiDpiStr the hi-DPI variation to generate, for example "2.5x".<p>
403     *
404     * @return a hi-DPI scaled version of the current image
405     */
406    public CmsJspImageBean createHiDpiVariation(String hiDpiStr) {
407
408        CmsJspImageBean result = null;
409        if (hiDpiStr.matches("^[0-9]+(.[0-9]+)?x$")) {
410
411            double multiplier = Double.valueOf(hiDpiStr.substring(0, hiDpiStr.length() - 1)).doubleValue();
412
413            int targetWidth = (int)Math.round(getScaler().getWidth() * multiplier);
414            int targetHeight = (int)Math.round(getScaler().getHeight() * multiplier);
415
416            CmsImageScaler targetScaler = createVariation(
417                getWidth(),
418                getHeight(),
419                getBaseScaler(),
420                targetWidth,
421                targetHeight,
422                getQuality());
423
424            if (targetScaler != null) {
425                result = createVariation(targetScaler);
426            }
427
428        } else {
429            if (LOG.isWarnEnabled()) {
430                LOG.warn(String.format("Illegal multiplier format: '%s' not usable for image scaling", hiDpiStr));
431            }
432        }
433        return result;
434    }
435
436    /**
437     * Creates a ratio scaled version of the current image.<p>
438     *
439     * @param ratioStr the rato variation to generate, for example "4-3" or "1-1".<p>
440     *
441     * @return a ratio scaled version of the current image
442     */
443    public CmsJspImageBean createRatioVariation(String ratioStr) {
444
445        CmsJspImageBean result = null;
446
447        try {
448            int i = ratioStr.indexOf('-');
449            if (i > 0) {
450                ratioStr = ratioStr.replace(',', '.');
451
452                double ratioW = Double.valueOf(ratioStr.substring(0, i)).doubleValue();
453                double ratioH = Double.valueOf(ratioStr.substring(i + 1)).doubleValue();
454
455                int targetWidth, targetHeight;
456
457                double ratioFactorW = getScaler().getWidth() / ratioW;
458                targetWidth = getScaler().getWidth();
459                targetHeight = (int)Math.round(ratioH * ratioFactorW);
460
461                if (targetHeight > getScaler().getHeight()) {
462                    double ratioFactorH = getScaler().getHeight() / ratioH;
463                    targetWidth = (int)Math.round(ratioW * ratioFactorH);
464                    targetHeight = getScaler().getHeight();
465                }
466
467                CmsImageScaler targetScaler = createVariation(
468                    getWidth(),
469                    getHeight(),
470                    getBaseScaler(),
471                    targetWidth,
472                    targetHeight,
473                    getQuality());
474
475                if (targetScaler != null) {
476                    result = createVariation(targetScaler);
477                    result.m_ratio = ratioStr;
478                    result.m_ratioHeightPercentage = calcRatioHeightPercentage(ratioW, ratioH);
479                }
480            }
481        } catch (NumberFormatException e) {
482            if (LOG.isWarnEnabled()) {
483                LOG.warn(String.format("Illegal ratio format: '%s' not usable for image scaling", ratioStr));
484            }
485        }
486
487        return result;
488    }
489
490    /**
491     * Creates a width scaled version of the current image.<p>
492     *
493     * @param widthStr the with variation to generate, for example "1078" or "800".<p>
494     *
495     * @return a width scaled version of the current image
496     */
497    public CmsJspImageBean createWidthVariation(String widthStr) {
498
499        CmsJspImageBean result = null;
500
501        try {
502
503            double baseRatio;
504            if ((getOriginalScaler().getFocalPoint() != null)
505                && checkCropRegionContainsFocalPoint(getScaler(), getOriginalScaler().getFocalPoint())) {
506                // We use scaling mode 8 if there is a focal point, and in this case,
507                // the correct aspect ratio is width x height, not cropWidth x cropHeight
508                // even if cropping is set
509                baseRatio = (double)getScaler().getWidth() / (double)getScaler().getHeight();
510            } else if (getScaler().isCropping()) {
511                // use the image crop with/height for aspect ratio calculation
512                baseRatio = (double)getScaler().getCropWidth() / (double)getScaler().getCropHeight();
513            } else {
514                // use the image original pixel width/height for aspect ratio calculation
515                baseRatio = (double)getScaler().getWidth() / (double)getScaler().getHeight();
516            }
517
518            // height is not set, calculate it with the given width and the aspect ratio
519            int targetWidth = Integer.valueOf(widthStr).intValue();
520            int targetHeight = (int)Math.round(targetWidth / baseRatio);
521
522            CmsImageScaler targetScaler = createVariation(
523                getWidth(),
524                getHeight(),
525                getBaseScaler(),
526                targetWidth,
527                targetHeight,
528                getQuality());
529
530            if (targetScaler != null) {
531                result = createVariation(targetScaler);
532            }
533
534        } catch (NumberFormatException e) {
535            if (LOG.isWarnEnabled()) {
536                LOG.warn(String.format("Illegal width format: '%s' not usable for image scaling", widthStr));
537            }
538        }
539
540        return result;
541    }
542
543    /**
544     * Returns the original pixel height of the image.<p>
545     *
546     * @return the original pixel height of the image
547     */
548    public int getHeight() {
549
550        return m_originalScaler.getHeight();
551    }
552
553    /**
554     * Returns a lazy initialized Map that provides access to ratio scaled instances of this image bean.<p>
555     *
556     * @return a lazy initialized Map that provides access to ratio scaled instances of this image bean
557     *
558     * @deprecated use {@link #getScaleHiDpi()} instead
559     */
560    @Deprecated
561    public Map<String, CmsJspImageBean> getHiDpiImages() {
562
563        return getScaleHiDpi();
564    }
565
566    /**
567     * Returns the basic source parameters for this image.<p>
568     *
569     * In case the image was cropped or otherwise manipulated,
570     * the values are created for the manipulated version.<p>
571     *
572     * The return form is "src='(srcUrl)' height='(h)' width='(w)'".<p>
573     *
574     * @return the basic source parameters for this image
575     */
576    public String getImgSrc() {
577
578        StringBuffer result = new StringBuffer(128);
579
580        // append the image source
581        result.append("src=\"");
582        result.append(getSrcUrl());
583        result.append("\"");
584        // append image width and height
585        result.append(" width=\"");
586        result.append(m_currentScaler.getWidth());
587        result.append("\"");
588        result.append(" height=\"");
589        result.append(m_currentScaler.getHeight());
590        result.append("\"");
591
592        return result.toString();
593    }
594
595    /**
596     * Returns the compression quality factor used for image generation.<p>
597     *
598     * @return the compression quality factor used for image generation
599     */
600    public int getQuality() {
601
602        return m_quality;
603    }
604
605    /**
606     * Returns the image ratio.<p>
607     *
608     * The ratio is in the form 'width-height', for example '4-3' or '16-9'.
609     * In case no ratio was set, the pixel dimensions of the image are returned.<p>
610     *
611     *  @return the image ratio
612     */
613    public String getRatio() {
614
615        if (m_ratio == null) {
616            m_ratio = "" + getScaler().getWidth() + "-" + getScaler().getHeight();
617        }
618        return m_ratio;
619    }
620
621    /**
622     * Returns the image height percentage relative to the image width as a String.<p>
623     *
624     * In case a ratio has been used to scale the image, the height percentage is
625     * calculated based on the ratio, not on the actual image pixel size.
626     * This is done to avoid rounding differences.<p>
627     *
628     *  @return the image height percentage relative to the image width
629     */
630    public String getRatioHeightPercentage() {
631
632        if (m_ratioHeightPercentage == null) {
633
634            m_ratioHeightPercentage = calcRatioHeightPercentage(getScaler().getWidth(), getScaler().getHeight());
635        }
636        return m_ratioHeightPercentage;
637    }
638
639    /**
640     * Returns the JSP access wrapped VFS resource for this image.<p>
641     *
642     * @return the JSP access wrapped VFS resource for this image
643     */
644    public CmsJspResourceWrapper getResource() {
645
646        return m_resource;
647    }
648
649    /**
650     * Returns a lazy initialized Map that provides access to hi-DPI scaled instances of this image bean.<p>
651     *
652     * <ul>
653     *   <li>key: the variant multiplier, e.g. "2x" (the common retina multiplier)</li>
654     *   <li>value: a CmsJspImageBean representing the hi-DPI variant</li>
655     * </ul>
656     *
657     * @return a lazy initialized Map that provides access to hi-DPI scaled instances of this image bean
658     */
659    public Map<String, CmsJspImageBean> getScaleHiDpi() {
660
661        if (m_hiDpiImages == null) {
662            m_hiDpiImages = CmsCollectionsGenericWrapper.createLazyMap(new CmsScaleHiDpiTransformer());
663        }
664        return m_hiDpiImages;
665    }
666
667    /**
668     * Returns the image scaler that is used for the scaled version of this image bean.<p>
669     *
670     * @return the image scaler that is used for the scaled version of this image bean
671     */
672    public CmsImageScaler getScaler() {
673
674        return m_currentScaler;
675    }
676
677    /**
678     * Returns a lazy initialized Map that provides access to ratio scaled instances of this image bean.<p>
679     *
680     * @return a lazy initialized Map that provides access to ratio scaled instances of this image bean
681     */
682    public Map<String, CmsJspImageBean> getScaleRatio() {
683
684        if (m_scaleRatio == null) {
685            m_scaleRatio = CmsCollectionsGenericWrapper.createLazyMap(new CmsScaleRatioTransformer());
686        }
687        return m_scaleRatio;
688    }
689
690    /**
691     * Returns a lazy initialized Map that provides access to width scaled instances of this image bean.<p>
692     *
693     * @return a lazy initialized Map that provides access to width scaled instances of this image bean
694     */
695    public Map<String, CmsJspImageBean> getScaleWidth() {
696
697        if (m_scaleWidth == null) {
698            m_scaleWidth = CmsCollectionsGenericWrapper.createLazyMap(new CmsScaleWidthTransformer());
699        }
700        return m_scaleWidth;
701    }
702
703    /**
704     * Generates a srcset attribute parameter list from all images added to this image bean.<p>
705     *
706     * @return a srcset attribute parameter list from all images added to this image bean
707     */
708    public String getSrcSet() {
709
710        StringBuffer result = new StringBuffer(128);
711        if (m_srcSet != null) {
712            int items = m_srcSet.size();
713            for (Map.Entry<Integer, CmsJspImageBean> entry : m_srcSet.entrySet()) {
714                CmsJspImageBean imageBean = entry.getValue();
715                // append the image source
716                result.append(imageBean.getSrcUrl());
717                result.append(" ");
718                // append width
719                result.append(imageBean.getScaler().getWidth());
720                result.append("w");
721                if (--items > 0) {
722                    result.append(", ");
723                }
724            }
725        }
726        return result.toString();
727    }
728
729    /**
730     * Generates a srcset attribute parameter for this image bean.<p>
731     *
732     * @return a srcset attribute parameter for this image bean
733     */
734    public String getSrcSetEntry() {
735
736        StringBuffer result = new StringBuffer(128);
737        if (m_currentScaler.isValid()) {
738            // append the image source
739            result.append(getSrcUrl());
740            result.append(" ");
741            // append width
742            result.append(m_currentScaler.getWidth());
743            result.append("w");
744        }
745        return result.toString();
746    }
747
748    /**
749     * Returns the source set map.<p>
750     *
751     * In case no source set entries have been added before, the map is not initialized and <code>null</code> is returned.
752     *
753     * @return the source set map
754     */
755    public Map<Integer, CmsJspImageBean> getSrcSetMap() {
756
757        return m_srcSet;
758    }
759
760    /**
761     * Returns the largest image from the generated source set.<p>
762     *
763     * In case the source set has not been initialized,
764     * it returns the instance itself.
765     *
766     * @return the largest image from the generated source set
767     */
768    public CmsJspImageBean getSrcSetMaxImage() {
769
770        CmsJspImageBean result = this;
771        if (m_srcSet != null) {
772            result = m_srcSet.lastEntry().getValue();
773        }
774        return result;
775    }
776
777    /**
778     * Returns the largest width value form the source set.<p>
779     *
780     * In case no source set entries have been added before, the map is not initialized and <code>0</code> is returned.
781     *
782     * @return the largest width value form the source set
783     */
784    public int getSrcSetMaxWidth() {
785
786        int result = 0;
787        if ((m_srcSet != null) && (m_srcSet.size() > 0)) {
788
789            result = m_srcSet.lastKey().intValue();
790        }
791        return result;
792    }
793
794    /**
795     * Getter for {@link #setSrcSets(CmsJspImageBean)} which returns this image bean.<p>
796     *
797     * Exists to make sure {@link #setSrcSets(CmsJspImageBean)} is available as property on a JSP.<p>
798     *
799     * @return this image bean
800     *
801     * @see CmsJspImageBean#getSrcSet()
802     * @see CmsJspImageBean#getSrcSetMap()
803     */
804    public CmsJspImageBean getSrcSets() {
805
806        return this;
807    }
808
809    /**
810     * Returns the image URL that may be used in img or picture tags.<p>
811     *
812     * @return the image URL
813     */
814    public String getSrcUrl() {
815
816        String imageSrc = getCmsObject().getSitePath(getResource());
817        if ((getScaler() != null) && getScaler().isValid()) {
818            // now append the scaler parameters if required
819            imageSrc += getScaler().toRequestParam();
820        }
821        return OpenCms.getLinkManager().substituteLink(getCmsObject(), imageSrc);
822    }
823
824    /**
825     * Returns the URI of the image in the OpenCms VFS.<p>
826     *
827     * @return the URI of the image in the OpenCms VFS
828     */
829    public String getVfsUri() {
830
831        return m_vfsUri;
832    }
833
834    /**
835     * Returns the original (unscaled) width of the image.<p>
836     *
837     * @return the original (unscaled) width of the image
838     */
839    public int getWidth() {
840
841        return m_originalScaler.getWidth();
842    }
843
844    /**
845     * Returns <code>true</code> if this image bean has been correctly initialized with an image VFS resource.<p>
846     *
847     * @return <code>true</code> if this image bean has been correctly initialized with an image VFS resource
848     */
849    public boolean isImage() {
850
851        return getOriginalScaler().isValid();
852    }
853
854    /**
855     * Returns <code>true</code> if the image has been scaled or otherwise processed.<p>
856     *
857     * @return <code>true</code> if the image has been scaled or otherwise processed
858     */
859    public boolean isScaled() {
860
861        return !m_currentScaler.isOriginalScaler();
862    }
863
864    /**
865     * Sets the compression quality factor to use for image generation.<p>
866     *
867     * @param quality the compression quality factor to use for image generation
868     */
869    public void setQuality(int quality) {
870
871        m_quality = quality;
872        getScaler().setQuality(m_quality);
873    }
874
875    /**
876     * Adjusts the quality settings for all image beans in the srcSet depending on the pixel count.<p>
877     *
878     * The idea is to make sure large pixel images use a higher JPEG compression in order to reduce the size.<p>
879     *
880     * The following quality settings are used depending on the image size:
881     * <ul>
882     * <li>larger then 1200 * 800: quality 75
883     * <li>larger then 1024 * 768: quality 80
884     * <li>otherwise: quality 85
885     * </ul>
886     *
887     */
888    public void setSrcSetQuality() {
889
890        if (m_srcSet != null) {
891
892            for (Map.Entry<Integer, CmsJspImageBean> entry : m_srcSet.entrySet()) {
893                CmsJspImageBean imageBean = entry.getValue();
894                int quality;
895                long pixel = imageBean.getScaler().getWidth() * imageBean.getScaler().getWidth();
896                if (pixel > 960000) {
897                    // image size 1200 * 800
898                    quality = 75;
899                } else if (pixel > 786432) {
900                    // image size 1024 * 768
901                    quality = 80;
902                } else {
903                    quality = 85;
904                }
905                imageBean.setQuality(quality);
906            }
907        }
908    }
909
910    /**
911     * Adds another image bean instance to the source set map of this bean.<p>
912     *
913     * @param imageBean the image bean to add
914     */
915    public void setSrcSets(CmsJspImageBean imageBean) {
916
917        if (m_srcSet == null) {
918            m_srcSet = new TreeMap<Integer, CmsJspImageBean>();
919        }
920        if ((imageBean != null) && imageBean.getScaler().isValid()) {
921            m_srcSet.put(Integer.valueOf(imageBean.getScaler().getWidth()), imageBean);
922        }
923    }
924
925    /**
926     * Sets the URI of the image in the OpenCms VFS.<p>
927     *
928     * @param vfsUri the URI of the image in the OpenCms VFS to set
929     */
930    public void setVfsUri(String vfsUri) {
931
932        m_vfsUri = vfsUri;
933    }
934
935    /**
936     * Returns the image source URL as String representation.<p>
937     *
938     * @return the image source URL
939     *
940     * @see #getSrcUrl()
941     */
942    @Override
943    public String toString() {
944
945        return getSrcUrl();
946    }
947
948    /**
949     * Returns the ratio height percentage of an image based on width and height.<p>
950     *
951     * @param width width to calculate percentage from
952     * @param height height to calculate percentage from
953     *
954     * @return the ratio height percentage of an image based on width and height
955     */
956    protected String calcRatioHeightPercentage(double width, double height) {
957
958        double p = Math.round((height / width) * 10000000.0) / 100000.0;
959        return String.valueOf(p) + "%";
960    }
961
962    /**
963     * Returns a variation of the current image scaled with the given scaler.<p>
964     *
965     * It is always the original image which is used as a base, never a scaled version.
966     * So for example if the image has been cropped by the user, the cropping are is ignored.<p>
967     *
968     * @param targetScaler contains the information about how to scale the image
969     *
970     * @return a variation of the current image scaled with the given scaler
971     */
972    protected CmsJspImageBean createVariation(CmsImageScaler targetScaler) {
973
974        CmsJspImageBean result = new CmsJspImageBean();
975
976        result.setCmsObject(getCmsObject());
977        result.setResource(getCmsObject(), getResource());
978        result.setOriginalScaler(getOriginalScaler());
979        result.setBaseScaler(getBaseScaler());
980        result.setVfsUri(getVfsUri());
981        result.setScaler(targetScaler);
982        result.setQuality(getQuality());
983
984        return result;
985    }
986
987    /**
988     * Sets the scaler that describes the basic adjustments (usually cropping) that have been set on the original image.<p>
989     *
990     * @return the scaler that describes the basic adjustments (usually cropping) that have been set on the original image
991     */
992    protected CmsImageScaler getBaseScaler() {
993
994        return m_baseScaler;
995    }
996
997    /**
998     * Returns the current OpenCms user context.<p>
999     *
1000     * @return the current OpenCms user context
1001     */
1002    protected CmsObject getCmsObject() {
1003
1004        return m_cms;
1005    }
1006
1007    /**
1008     * Returns the image scaler that describes the original proportions of this image.<p>
1009     *
1010     * @return the image scaler that describes the original proportions of this image
1011     */
1012    protected CmsImageScaler getOriginalScaler() {
1013
1014        return m_originalScaler;
1015    }
1016
1017    /**
1018     * Returns this instance bean, required for the transformers.<p>
1019     *
1020     * @return this instance bean
1021     */
1022    protected CmsJspImageBean getSelf() {
1023
1024        return this;
1025    }
1026
1027    /**
1028     * Initializes this new image bean based on a VFS resource and optional scaler parameters.<p>
1029     *
1030     * @param cms the current OpenCms user context
1031     * @param imageRes the VFS resource to read the image from
1032     * @param scaleParams optional scaler parameters to apply to the VFS resource
1033     */
1034    protected void init(CmsObject cms, CmsResource imageRes, String scaleParams) {
1035
1036        setCmsObject(cms);
1037
1038        // set VFS URI without scaling parameters
1039        setResource(cms, imageRes);
1040        setVfsUri(cms.getRequestContext().getSitePath(imageRes));
1041
1042        // the originalScaler reads the image dimensions from the VFS properties
1043        CmsImageScaler originalScaler = new CmsImageScaler(cms, getResource());
1044        // set original scaler
1045        setOriginalScaler(originalScaler);
1046
1047        // set base scaler
1048        CmsImageScaler baseScaler = originalScaler;
1049        if (scaleParams != null) {
1050            // scale parameters have been set
1051            baseScaler = new CmsImageScaler(scaleParams);
1052            baseScaler.setFocalPoint(originalScaler.getFocalPoint());
1053        }
1054
1055        setBaseScaler(baseScaler);
1056
1057        // set the current scaler to the base scaler
1058        setScaler(baseScaler);
1059    }
1060
1061    /**
1062     * Returns the scaler that describes the basic adjustments (usually cropping) that have been set on the original image.<p>
1063     *
1064     * @param baseScaler the scaler that describes the basic adjustments (usually cropping) that have been set on the original image
1065     */
1066    protected void setBaseScaler(CmsImageScaler baseScaler) {
1067
1068        m_baseScaler = baseScaler;
1069    }
1070
1071    /**
1072     * Sets the current OpenCms user context.<p>
1073     *
1074     * @param cms the current OpenCms user context to set
1075     */
1076    protected void setCmsObject(CmsObject cms) {
1077
1078        m_cms = cms;
1079    }
1080
1081    /**
1082     * Sets the scaler that describes the original proportions of this image.<p>
1083     *
1084     * @param originalScaler the scaler that describes the original proportions of this image
1085     */
1086    protected void setOriginalScaler(CmsImageScaler originalScaler) {
1087
1088        m_originalScaler = originalScaler;
1089    }
1090
1091    /**
1092     * Sets the CmsResource for this image.<p>
1093     *
1094     * @param cms the current OpenCms user context, required for wrapping the resource
1095     * @param resource the VFS resource for this image
1096     */
1097    protected void setResource(CmsObject cms, CmsResource resource) {
1098
1099        m_resource = CmsJspResourceWrapper.wrap(cms, resource);
1100    }
1101
1102    /**
1103     * Sets the image scaler that was used to create this image.<p>
1104     *
1105     * @param scaler the image scaler that was used to create this image.
1106     */
1107    protected void setScaler(CmsImageScaler scaler) {
1108
1109        m_currentScaler = scaler;
1110    }
1111}