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.ade.galleries.client.preview;
029
030import org.opencms.ade.galleries.client.preview.ui.CmsImagePreviewDialog;
031import org.opencms.ade.galleries.shared.CmsImageInfoBean;
032import org.opencms.gwt.client.CmsCoreProvider;
033import org.opencms.gwt.client.util.CmsClientStringUtil;
034import org.opencms.gwt.client.util.I_CmsSimpleCallback;
035import org.opencms.util.CmsStringUtil;
036
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.List;
040import java.util.Map;
041
042import com.google.gwt.event.logical.shared.ValueChangeEvent;
043import com.google.gwt.event.logical.shared.ValueChangeHandler;
044import com.google.gwt.user.client.ui.Image;
045import com.google.gwt.user.client.ui.Widget;
046
047import elemental2.dom.HTMLImageElement;
048import jsinterop.base.Js;
049
050/**
051 * Image preview dialog controller handler.<p>
052 *
053 * Delegates the actions of the preview controller to the preview dialog.
054 *
055 * @since 8.0.0
056 */
057public class CmsImagePreviewHandler extends A_CmsPreviewHandler<CmsImageInfoBean>
058implements ValueChangeHandler<CmsCroppingParamBean> {
059
060    /** Enumeration of image tag attribute names. */
061    public enum Attribute {
062        /** Image align attribute. */
063        align,
064        /** Image alt attribute. */
065        alt,
066        /** Image class attribute. */
067        clazz,
068        /** Image copyright info. */
069        copyright,
070        /** Image direction attribute. */
071        dir,
072        /** No image selected if this attribute is present. */
073        emptySelection,
074        /** The image hash. */
075        hash,
076        /** Image height attribute. */
077        height,
078        /** Image hspace attribute. */
079        hspace,
080        /** Image id attribute. */
081        id,
082        /** Image copyright flag. */
083        insertCopyright,
084        /** Image link original flag. */
085        insertLinkOrig,
086        /** Image spacing flag. */
087        insertSpacing,
088        /** Image subtitle flag. */
089        insertSubtitle,
090        /** Image language attribute. */
091        lang,
092        /** Image link path. */
093        linkPath,
094        /** Image link target. */
095        linkTarget,
096        /** Image longDesc attribute. */
097        longDesc,
098        /** Image style attribute. */
099        style,
100        /** Image title attribute. */
101        title,
102        /** Image vspace attribute. */
103        vspace,
104        /** Image width attribute. */
105        width
106    }
107
108    /**
109     * Encapsulates information used to update the preview image's scaling parameters.
110     */
111    public static class PreviewImageUpdate {
112
113        /** Normal height. */
114        private int m_height;
115
116        /** High resolution scaling parameters. */
117        private String m_highResPreview;
118
119        /** Normal preview scaling parameters. */
120        private String m_preview;
121
122        /** Normal width. */
123        private int m_width;
124
125        /**
126         * Creates a new instance.
127         *
128         * @param preview the normal preview scaling parameters
129         * @param highResPreview the high resolution scaling parameters
130         * @param width the normal width
131         * @param height the normal height
132         */
133        public PreviewImageUpdate(String preview, String highResPreview, int width, int height) {
134
135            super();
136            m_preview = preview;
137            m_highResPreview = highResPreview;
138            m_width = width;
139            m_height = height;
140        }
141
142        /**
143         * Updates the given image with information from this object.
144         *
145         * @param image the image to update
146         * @param src the image base URL
147         * @param isSvg true if the image is an SVG
148         */
149        public void applyToImage(Image image, String src, boolean isSvg, Widget container) {
150
151            HTMLImageElement imgElement = Js.cast(image.getElement());
152            long time = System.currentTimeMillis();
153            if (!isSvg) {
154                int parentWidth = container.getElement().getClientWidth();
155                int parentHeight = container.getElement().getClientHeight();
156                int effectiveWidth = m_width;
157                if ((parentWidth > effectiveWidth) && ((parentWidth - effectiveWidth) <= 3)) {
158                    effectiveWidth = parentWidth;
159                }
160                int effectiveHeight = m_height;
161                if ((parentHeight > effectiveHeight) && ((parentHeight - effectiveHeight) <= 3)) {
162                    effectiveHeight = parentHeight;
163                }
164
165                imgElement.setAttribute("width", "" + effectiveWidth);
166                imgElement.setAttribute("height", "" + effectiveHeight);
167            }
168            imgElement.src = src + "?" + appendQuality(m_preview) + "&time=" + time;
169            imgElement.removeAttribute("srcset");
170            if (!isSvg) {
171                if (m_highResPreview != null) {
172                    imgElement.srcset = src + "?" + appendQuality(m_highResPreview) + "&time=" + time + " 2x";
173                }
174            }
175
176        }
177
178    }
179
180    /** The image container height. */
181    private int m_containerHeight;
182
183    /** The image container width. */
184    private int m_containerWidth;
185
186    /** List of handlers for cropping changes. */
187    private List<Runnable> m_croppingHandlers = new ArrayList<>();
188
189    /** The cropping parameter. */
190    private CmsCroppingParamBean m_croppingParam;
191
192    /** The image format handler. */
193    private CmsImageFormatHandler m_formatHandler;
194
195    /** List of handlers for focal point changes. */
196    private List<Runnable> m_imagePointHandlers = new ArrayList<>();
197
198    /** The focal point controller. */
199    private CmsFocalPointController m_pointController;
200
201    /** The preview dialog. */
202    private CmsImagePreviewDialog m_previewDialog;
203
204    /**
205     * Constructor.<p>
206     *
207     * @param resourcePreview the resource preview instance
208     */
209    public CmsImagePreviewHandler(CmsImageResourcePreview resourcePreview) {
210
211        super(resourcePreview);
212        m_previewDialog = resourcePreview.getPreviewDialog();
213        m_pointController = new CmsFocalPointController(
214            () -> m_croppingParam,
215            this::getImageInfo,
216            this::onImagePointChanged);
217    }
218
219    /**
220     * Appends quality parameter to a set of scaling parameters, unless the input is the empty string or already contains a quality parameter.
221     *
222     * @param text the input scaling parameters
223     * @return the modified scaling parameters
224     */
225    public static final String appendQuality(String text) {
226
227        if (CmsStringUtil.isEmpty(text) || text.contains("q:")) {
228            return text;
229        } else {
230            return text + ",q:85";
231        }
232    }
233
234    /**
235     * Adds a handler for cropping changes.<p>
236     *
237     * @param action the handler to add
238     */
239    public void addCroppingChangeHandler(Runnable action) {
240
241        m_croppingHandlers.add(action);
242    }
243
244    /**
245     * Adds a handler for focal point changes.<p>
246     *
247     * @param onImagePointChanged the handler to add
248     */
249    public void addImagePointChangeHandler(Runnable onImagePointChanged) {
250
251        m_imagePointHandlers.add(onImagePointChanged);
252    }
253
254    /**
255     * Returns the image cropping parameter bean.<p>
256     *
257     * @return the image cropping parameter bean
258     */
259    public CmsCroppingParamBean getCroppingParam() {
260
261        return m_croppingParam;
262    }
263
264    /**
265     * Gets the focal point controller.<p>
266     *
267     * @return the focal point controller
268     */
269    public CmsFocalPointController getFocalPointController() {
270
271        return m_pointController;
272    }
273
274    /**
275     * Gets the format handler.<p>
276     *
277     * @return the format handler
278     */
279    public CmsImageFormatHandler getFormatHandler() {
280
281        return m_formatHandler;
282
283    }
284
285    /**
286     * Returns the name of the currently selected image format.<p>
287     *
288     * @return the format name
289     */
290    public String getFormatName() {
291
292        String result = "";
293        if ((m_formatHandler != null) && (m_formatHandler.getCurrentFormat() != null)) {
294            result = m_formatHandler.getCurrentFormat().getName();
295        }
296        return result;
297    }
298
299    /**
300     * Returns image tag attributes to set for editor plugins.<p>
301     *
302     * @param callback the callback to execute
303     */
304    public void getImageAttributes(I_CmsSimpleCallback<Map<String, String>> callback) {
305
306        Map<String, String> result = new HashMap<String, String>();
307        result.put(Attribute.hash.name(), String.valueOf(getImageIdHash()));
308        m_formatHandler.getImageAttributes(result);
309        m_previewDialog.getImageAttributes(result, callback);
310    }
311
312    /**
313     * Returns the structure id hash of the previewed image.<p>
314     *
315     * @return the structure id hash
316     */
317    public int getImageIdHash() {
318
319        return m_resourceInfo.getHash();
320    }
321
322    /**
323     * Gets the image information.<p>
324     *
325     * @return the image information
326     */
327    public CmsImageInfoBean getImageInfo() {
328
329        return m_resourceInfo;
330    }
331
332    /**
333     * Gets the information to update the preview image.
334     *
335     * @param imageHeight the original image height
336     * @param imageWidth the original image width
337     * @return the preview update information
338     */
339    public PreviewImageUpdate getPreviewImageUpdate(int imageHeight, int imageWidth) {
340
341        String lowRes = getPreviewScaleParam(imageHeight, imageWidth, 1);
342        String highRes = getPreviewScaleParam(imageHeight, imageWidth, 2);
343        Map<String, String> lowResMap = parseScalingParams(lowRes);
344        int wLow = getScalerParameter(lowResMap, "w", imageWidth);
345        int hLow = getScalerParameter(lowResMap, "h", imageHeight);
346        return new PreviewImageUpdate(lowRes, highRes, wLow, hLow);
347
348    }
349
350    /**
351     * Returns the cropping parameter.<p>
352     *
353     * @param imageHeight the original image height
354     * @param imageWidth the original image width
355     * @param density the pixel density (acts as a multiplier for available space)
356     *
357     * @return the cropping parameter
358     */
359    public String getPreviewScaleParam(int imageHeight, int imageWidth, int density) {
360
361        int maxHeight = m_containerHeight * density;
362        int maxWidth = m_containerWidth * density;
363
364        if ((m_croppingParam != null) && (m_croppingParam.isCropped() || m_croppingParam.isScaled())) {
365            // NOTE: getREstrictedSizeScaleParam does not work correctly if there isn't actually any cropping/scaling, so we explicitly don't use it in this case
366            return m_croppingParam.getRestrictedSizeScaleParam(maxHeight, maxWidth);
367        }
368        if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) {
369            return ""; // dummy parameter, doesn't actually do anything
370        }
371        CmsCroppingParamBean restricted = new CmsCroppingParamBean();
372
373        boolean tooHigh = imageHeight > maxHeight;
374        boolean tooWide = imageWidth > maxWidth;
375        double shrinkX = (1.0 * imageWidth) / maxWidth;
376        double shrinkY = (1.0 * imageHeight) / maxHeight;
377        double aspectRatio = (1.0 * imageWidth) / imageHeight;
378        if (tooHigh && tooWide) {
379            if (shrinkX > shrinkY) {
380                restricted.setTargetWidth(maxWidth);
381                restricted.setTargetHeight((int)(maxWidth / aspectRatio));
382            } else {
383                restricted.setTargetHeight(maxHeight);
384                restricted.setTargetWidth((int)(maxHeight * aspectRatio));
385            }
386        } else if (tooWide) {
387            restricted.setTargetWidth(maxWidth);
388            restricted.setTargetHeight((int)(maxWidth / aspectRatio));
389        } else if (tooHigh) {
390            restricted.setTargetHeight(maxHeight);
391            restricted.setTargetWidth((int)(maxHeight * aspectRatio));
392        } else {
393            restricted.setTargetWidth(imageWidth);
394            restricted.setTargetHeight(imageHeight);
395        }
396        return restricted.toString();
397    }
398
399    /**
400     * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)
401     */
402    public void onValueChange(ValueChangeEvent<CmsCroppingParamBean> event) {
403
404        m_croppingParam = event.getValue();
405        String viewLink = m_resourcePreview.getViewLink();
406        if (viewLink == null) {
407            viewLink = CmsCoreProvider.get().link(m_resourcePreview.getResourcePath());
408        }
409        PreviewImageUpdate previewUpdate = getPreviewImageUpdate(
410            m_croppingParam.getOrgHeight(),
411            m_croppingParam.getOrgWidth());
412        boolean isSvg = CmsClientStringUtil.checkIsPathOrLinkToSvg(m_resourcePreview.getResourcePath());
413        previewUpdate.applyToImage(
414            m_previewDialog.getPreviewImage(),
415            viewLink,
416            isSvg,
417            m_previewDialog.getPreviewImage().getParent());
418        onCroppingChanged();
419    }
420
421    /**
422     * Sets the image format handler.<p>
423     *
424     * @param formatHandler the format handler
425     */
426    public void setFormatHandler(CmsImageFormatHandler formatHandler) {
427
428        m_formatHandler = formatHandler;
429        m_croppingParam = m_formatHandler.getCroppingParam();
430        m_formatHandler.addValueChangeHandler(this);
431        onCroppingChanged();
432    }
433
434    /**
435     *
436     * Sets the dimensions of the area the image is going to be placed in.
437     *
438     * @param offsetWidth the container width
439     * @param offsetHeight the container height
440     */
441    public void setImageContainerSize(int offsetWidth, int offsetHeight) {
442
443        m_containerWidth = offsetWidth;
444        m_containerHeight = offsetHeight;
445    }
446
447    /**
448     * Helper method for getting an integer-valued scaler parameter from a map of parameters, with a default value that should be returned if the map doesn't contain the parameter.
449     *
450     * @param scalerParams the map of scaler parameters
451     * @param key the map key
452     * @param defaultValue the value to return if the map doesn't contain a value for the key
453     *
454     * @return the value of the scaler parameter
455     */
456    private int getScalerParameter(Map<String, String> scalerParams, String key, int defaultValue) {
457
458        String value = scalerParams.get(key);
459        if (value != null) {
460            return Integer.parseInt(value);
461        } else {
462            return defaultValue;
463        }
464    }
465
466    /**
467     * Calls all cropping change handlers.
468     */
469    private void onCroppingChanged() {
470
471        for (Runnable action : m_croppingHandlers) {
472            action.run();
473        }
474    }
475
476    /**
477     * Calls all focal point change handlers.<p>
478     */
479    private void onImagePointChanged() {
480
481        for (Runnable handler : m_imagePointHandlers) {
482            handler.run();
483        }
484
485    }
486
487    /**
488     * Parse scaling parameters as a map.
489     *
490     * @param params the scaling parameters
491     * @return the scaling parameters as a map
492     */
493    private Map<String, String> parseScalingParams(String params) {
494
495        final String prefix = "__scale=";
496        if (params.startsWith(prefix)) {
497            params = params.substring(prefix.length());
498        }
499        return CmsStringUtil.splitAsMap(params, ",", ":");
500    }
501
502}