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 GmbH & Co. KG, 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.file.types;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.db.CmsSecurityManager;
032import org.opencms.file.CmsFile;
033import org.opencms.file.CmsObject;
034import org.opencms.file.CmsProperty;
035import org.opencms.file.CmsPropertyDefinition;
036import org.opencms.file.CmsResource;
037import org.opencms.file.CmsResourceFilter;
038import org.opencms.file.CmsVfsException;
039import org.opencms.loader.CmsDumpLoader;
040import org.opencms.loader.CmsImageLoader;
041import org.opencms.loader.CmsImageScaler;
042import org.opencms.main.CmsException;
043import org.opencms.main.CmsLog;
044import org.opencms.main.OpenCms;
045import org.opencms.security.CmsPermissionSet;
046import org.opencms.security.CmsSecurityException;
047import org.opencms.util.CmsStringUtil;
048
049import java.io.ByteArrayInputStream;
050import java.util.ArrayList;
051import java.util.HashMap;
052import java.util.List;
053import java.util.Map;
054import java.util.Objects;
055
056import org.apache.commons.logging.Log;
057
058import org.dom4j.Document;
059import org.dom4j.Element;
060import org.dom4j.io.SAXReader;
061
062/**
063 * Resource type descriptor for the type "image".<p>
064 *
065 * @since 6.0.0
066 */
067public class CmsResourceTypeImage extends A_CmsResourceType {
068
069    /**
070     * A data container for image size and scale operations.<p>
071     */
072    protected static class CmsImageAdjuster {
073
074        /** The image byte content. */
075        private byte[] m_content;
076
077        /** The (optional) image scaler that contains the image downscale settings. */
078        private CmsImageScaler m_imageDownScaler;
079
080        /** The image properties. */
081        private List<CmsProperty> m_properties;
082
083        /** The image root path. */
084        private String m_rootPath;
085
086        /**
087         * Creates a new image data container.<p>
088         *
089         * @param content the image byte content
090         * @param rootPath the image root path
091         * @param properties the image properties
092         * @param downScaler the (optional) image scaler that contains the image downscale settings
093         */
094        public CmsImageAdjuster(
095            byte[] content,
096            String rootPath,
097            List<CmsProperty> properties,
098            CmsImageScaler downScaler) {
099
100            m_content = content;
101            m_rootPath = rootPath;
102            m_properties = properties;
103            m_imageDownScaler = downScaler;
104        }
105
106        /**
107         * Calculates the image size and adjusts the image dimensions (if required) accoring to the configured
108         * image downscale settings.<p>
109         *
110         * The image dimensions are always calculated from the given image. The internal list of properties is updated
111         * with a value for <code>{@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}</code> that
112         * contains the calculated image dimensions.<p>
113         */
114        public void adjust() {
115
116            CmsImageScaler scaler = new CmsImageScaler(getContent(), getRootPath());
117            if (!scaler.isValid()) {
118                // error calculating image dimensions - this image can't be scaled or resized
119                return;
120            }
121
122            // check if the image is to big and needs to be rescaled
123            if (scaler.isDownScaleRequired(m_imageDownScaler)) {
124                // image is to big, perform rescale operation
125                CmsImageScaler downScaler = scaler.getDownScaler(m_imageDownScaler);
126                // perform the rescale using the adjusted size
127                m_content = downScaler.scaleImage(m_content, m_rootPath);
128                // image size has been changed, adjust the scaler for later setting of properties
129                scaler.setHeight(downScaler.getHeight());
130                scaler.setWidth(downScaler.getWidth());
131            }
132
133            CmsProperty p = new CmsProperty(CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, null, scaler.toString());
134            // create the new property list if required (don't modify the original List)
135            List<CmsProperty> result = new ArrayList<CmsProperty>();
136            if ((m_properties != null) && (m_properties.size() > 0)) {
137                result.addAll(m_properties);
138                result.remove(p);
139            }
140            // add the updated property
141            result.add(p);
142            // store the changed properties
143            m_properties = result;
144        }
145
146        /**
147         * Returns the image content.<p>
148         *
149         * @return the image content
150         */
151        public byte[] getContent() {
152
153            return m_content;
154        }
155
156        /**
157         * Returns the image properties.<p>
158         *
159         * @return the image properties
160         */
161        public List<CmsProperty> getProperties() {
162
163            return m_properties;
164        }
165
166        /**
167         * Returns the image VFS root path.<p>
168         *
169         * @return the image VFS root path
170         */
171        public String getRootPath() {
172
173            return m_rootPath;
174        }
175    }
176
177    /**
178     * Helper class for parsing SVG sizes.<p>
179     *
180     * Note: This is *not* intended as a general purpose tool, it is only used for parsing SVG sizes
181     * as part of the image.size property determination.
182     */
183    private static class SvgSize {
184
185        /** The numeric value of the size. */
186        private double m_size;
187
188        /** The unit of the size. */
189        private String m_unit;
190
191        /**
192         * Parses the SVG size.<p>
193         *
194         * @param s the string containing the size
195         *
196         * @return the parsed size
197         */
198        public static SvgSize parse(String s) {
199
200            if (CmsStringUtil.isEmptyOrWhitespaceOnly(s)) {
201                return null;
202            }
203            s = s.trim();
204            double length = -1;
205            int unitPos;
206            String unit = "";
207            // find longest prefix of s that can be parsed as a number, use the remaining part as the unit
208            for (unitPos = s.length(); unitPos >= 0; unitPos--) {
209                String prefix = s.substring(0, unitPos);
210                unit = s.substring(unitPos);
211                try {
212                    length = Double.parseDouble(prefix);
213                    break;
214                } catch (NumberFormatException e) {
215                    // ignore
216                }
217            }
218            if (length < 0) {
219                LOG.warn("Invalid string for SVG size: " + s);
220                return null;
221            }
222            SvgSize result = new SvgSize();
223            result.m_size = length;
224            result.m_unit = unit;
225            return result;
226        }
227
228        /**
229         * Gets the numeric value of the size.<p>
230         *
231         * @return the size
232         */
233        public double getSize() {
234
235            return m_size;
236        }
237
238        /**
239         * Gets the unit of the size.<p>
240         *
241         * @return the unit
242         */
243        public String getUnit() {
244
245            return m_unit;
246
247        }
248
249        /**
250         * @see java.lang.Object#toString()
251         */
252        @Override
253        public String toString() {
254
255            return m_size + m_unit;
256        }
257    }
258
259    /** The log object for this class. */
260    public static final Log LOG = CmsLog.getLog(CmsResourceTypeImage.class);
261
262    /**
263     * The value for the {@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE} property if resources in
264     * a folder should never be downscaled.<p>
265     */
266    public static final String PROPERTY_VALUE_UNLIMITED = "unlimited";
267
268    /** The default image preview provider. */
269    private static final String GALLERY_PREVIEW_PROVIDER = "org.opencms.ade.galleries.preview.CmsImagePreviewProvider";
270
271    /** The image scaler for the image downscale operation (if configured). */
272    private static CmsImageScaler m_downScaler;
273
274    /** Indicates that the static configuration of the resource type has been frozen. */
275    private static boolean m_staticFrozen;
276
277    /** The static resource loader id of this resource type. */
278    private static int m_staticLoaderId;
279
280    /** The static type id of this resource type. */
281    private static int m_staticTypeId;
282
283    /** The type id of this resource type. */
284    private static final int RESOURCE_TYPE_ID = 3;
285
286    /** The name of this resource type. */
287    private static final String RESOURCE_TYPE_NAME = "image";
288
289    /** The serial version id. */
290    private static final long serialVersionUID = -8708850913653288684L;
291
292    /**
293     * Default constructor, used to initialize member variables.<p>
294     */
295    public CmsResourceTypeImage() {
296
297        super();
298        m_typeId = RESOURCE_TYPE_ID;
299        m_typeName = RESOURCE_TYPE_NAME;
300    }
301
302    /**
303     * Returns the image downscaler to use when writing an image resource to the given root path.<p>
304     *
305     * If <code>null</code> is returned, image downscaling must not be used for the resource with the given path.
306     * This may be the case if image downscaling is not configured at all, or if image downscaling has been disabled
307     * for the parent folder by setting the folders property {@link CmsPropertyDefinition#PROPERTY_IMAGE_SIZE}
308     * to the value {@link #PROPERTY_VALUE_UNLIMITED}.<p>
309     *
310     * @param cms the current OpenCms user context
311     * @param rootPath the root path of the resource to write
312     *
313     * @return the downscaler to use, or <code>null</code> if no downscaling is required for the resource
314     */
315    public static CmsImageScaler getDownScaler(CmsObject cms, String rootPath) {
316
317        if (m_downScaler == null) {
318            // downscaling is not configured at all
319            return null;
320        }
321        // try to read the image.size property from the parent folder
322        String parentFolder = CmsResource.getParentFolder(rootPath);
323        parentFolder = cms.getRequestContext().removeSiteRoot(parentFolder);
324        try {
325            CmsProperty fileSizeProperty = cms.readPropertyObject(
326                parentFolder,
327                CmsPropertyDefinition.PROPERTY_IMAGE_SIZE,
328                true);
329            if (!fileSizeProperty.isNullProperty()) {
330                // image.size property has been set
331                String value = fileSizeProperty.getValue().trim();
332                if (CmsStringUtil.isNotEmpty(value)) {
333                    if (PROPERTY_VALUE_UNLIMITED.equals(value)) {
334                        // in this case no downscaling must be done
335                        return null;
336                    } else {
337                        CmsImageScaler scaler = new CmsImageScaler(value);
338                        if (scaler.isValid()) {
339                            // special folder based scaler settings have been set
340                            return scaler;
341                        }
342                    }
343                }
344            }
345        } catch (CmsException e) {
346            // ignore, continue with given downScaler
347        }
348        return (CmsImageScaler)m_downScaler.clone();
349    }
350
351    /**
352     * Returns the static type id of this (default) resource type.<p>
353     *
354     * @return the static type id of this (default) resource type
355     */
356    public static int getStaticTypeId() {
357
358        return m_staticTypeId;
359    }
360
361    /**
362     * Returns the static type name of this (default) resource type.<p>
363     *
364     * @return the static type name of this (default) resource type
365     */
366    public static String getStaticTypeName() {
367
368        return RESOURCE_TYPE_NAME;
369    }
370
371    /**
372     * @see org.opencms.file.types.I_CmsResourceType#createResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, java.lang.String, byte[], java.util.List)
373     */
374    @Override
375    public CmsResource createResource(
376        CmsObject cms,
377        CmsSecurityManager securityManager,
378        String resourcename,
379        byte[] content,
380        List<CmsProperty> properties)
381    throws CmsException {
382
383        if (resourcename.toLowerCase().endsWith(".svg")) {
384            List<CmsProperty> prop2 = tryAddImageSizeFromSvg(content, properties);
385            properties = prop2;
386        } else if (CmsImageLoader.isEnabled()) {
387            String rootPath = cms.getRequestContext().addSiteRoot(resourcename);
388            // get the downscaler to use
389            CmsImageScaler downScaler = getDownScaler(cms, rootPath);
390            // create a new image scale adjuster
391            CmsImageAdjuster adjuster = new CmsImageAdjuster(content, rootPath, properties, downScaler);
392            // update the image scale adjuster - this will calculate the image dimensions and (optionally) downscale the size
393            adjuster.adjust();
394            // continue with the updated content and properties
395            content = adjuster.getContent();
396            properties = adjuster.getProperties();
397        }
398        return super.createResource(cms, securityManager, resourcename, content, properties);
399    }
400
401    /**
402     * @see org.opencms.file.types.I_CmsResourceType#getGalleryPreviewProvider()
403     */
404    @Override
405    public String getGalleryPreviewProvider() {
406
407        if (m_galleryPreviewProvider == null) {
408            m_galleryPreviewProvider = getConfiguration().getString(
409                CONFIGURATION_GALLERY_PREVIEW_PROVIDER,
410                GALLERY_PREVIEW_PROVIDER);
411        }
412        return m_galleryPreviewProvider;
413    }
414
415    /**
416     * @see org.opencms.file.types.I_CmsResourceType#getLoaderId()
417     */
418    @Override
419    public int getLoaderId() {
420
421        return m_staticLoaderId;
422    }
423
424    /**
425     * @see org.opencms.file.types.I_CmsResourceType#importResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, java.lang.String, org.opencms.file.CmsResource, byte[], java.util.List)
426     */
427    @Override
428    public CmsResource importResource(
429        CmsObject cms,
430        CmsSecurityManager securityManager,
431        String resourcename,
432        CmsResource resource,
433        byte[] content,
434        List<CmsProperty> properties)
435    throws CmsException {
436
437        if (resourcename.toLowerCase().endsWith(".svg")) {
438            properties = tryAddImageSizeFromSvg(content, properties);
439        } else if (CmsImageLoader.isEnabled()) {
440            // siblings have null content in import
441            if (content != null) {
442                // get the downscaler to use
443                CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath());
444                // create a new image scale adjuster
445                CmsImageAdjuster adjuster = new CmsImageAdjuster(
446                    content,
447                    resource.getRootPath(),
448                    properties,
449                    downScaler);
450                // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size
451                adjuster.adjust();
452                // continue with the updated content and properties
453                content = adjuster.getContent();
454                properties = adjuster.getProperties();
455            }
456        }
457        return super.importResource(cms, securityManager, resourcename, resource, content, properties);
458    }
459
460    /**
461     * @see org.opencms.file.types.A_CmsResourceType#initConfiguration(java.lang.String, java.lang.String, String)
462     */
463    @Override
464    public void initConfiguration(String name, String id, String className) throws CmsConfigurationException {
465
466        if ((OpenCms.getRunLevel() > OpenCms.RUNLEVEL_2_INITIALIZING) && m_staticFrozen) {
467            // configuration already frozen
468            throw new CmsConfigurationException(
469                Messages.get().container(
470                    Messages.ERR_CONFIG_FROZEN_3,
471                    this.getClass().getName(),
472                    getStaticTypeName(),
473                    new Integer(getStaticTypeId())));
474        }
475
476        if (!RESOURCE_TYPE_NAME.equals(name)) {
477            // default resource type MUST have default name
478            throw new CmsConfigurationException(
479                Messages.get().container(
480                    Messages.ERR_INVALID_RESTYPE_CONFIG_NAME_3,
481                    this.getClass().getName(),
482                    RESOURCE_TYPE_NAME,
483                    name));
484        }
485
486        // freeze the configuration
487        m_staticFrozen = true;
488
489        super.initConfiguration(RESOURCE_TYPE_NAME, id, className);
490        // set static members with values from the configuration
491        m_staticTypeId = m_typeId;
492
493        if (CmsImageLoader.isEnabled()) {
494            // the image loader is enabled, image operations are supported
495            m_staticLoaderId = CmsImageLoader.RESOURCE_LOADER_ID_IMAGE_LOADER;
496            // set the maximum size scaler
497            String downScaleParams = CmsImageLoader.getDownScaleParams();
498            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(downScaleParams)) {
499                m_downScaler = new CmsImageScaler(downScaleParams);
500                if (!m_downScaler.isValid()) {
501                    // ignore invalid parameters
502                    m_downScaler = null;
503                }
504            }
505        } else {
506            // no image operations are supported, use dump loader
507            m_staticLoaderId = CmsDumpLoader.RESOURCE_LOADER_ID;
508            // disable maximum image size operation
509            m_downScaler = null;
510        }
511    }
512
513    /**
514     * @see org.opencms.file.types.I_CmsResourceType#replaceResource(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsResource, int, byte[], java.util.List)
515     */
516    @Override
517    public void replaceResource(
518        CmsObject cms,
519        CmsSecurityManager securityManager,
520        CmsResource resource,
521        int type,
522        byte[] content,
523        List<CmsProperty> properties)
524    throws CmsException {
525
526        if (resource.getRootPath().toLowerCase().endsWith(".svg")) {
527            List<CmsProperty> newProperties = tryAddImageSizeFromSvg(content, properties);
528            if (properties != newProperties) { // yes, we actually do want to compare object identity here
529                writePropertyObjects(cms, securityManager, resource, newProperties);
530            }
531        } else if (CmsImageLoader.isEnabled()) {
532            // check if the user has write access and if resource is locked
533            // done here so that no image operations are performed in case no write access is granted
534            securityManager.checkPermissions(
535                cms.getRequestContext(),
536                resource,
537                CmsPermissionSet.ACCESS_WRITE,
538                true,
539                CmsResourceFilter.ALL);
540
541            // get the downscaler to use
542            CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath());
543            // create a new image scale adjuster
544            CmsImageAdjuster adjuster = new CmsImageAdjuster(content, resource.getRootPath(), properties, downScaler);
545            // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size
546            adjuster.adjust();
547            // continue with the updated content
548            content = adjuster.getContent();
549            if (adjuster.getProperties() != null) {
550                // write properties
551                writePropertyObjects(cms, securityManager, resource, adjuster.getProperties());
552            }
553        }
554        super.replaceResource(cms, securityManager, resource, type, content, properties);
555    }
556
557    /**
558     * @see org.opencms.file.types.I_CmsResourceType#writeFile(org.opencms.file.CmsObject, org.opencms.db.CmsSecurityManager, org.opencms.file.CmsFile)
559     */
560    @Override
561    public CmsFile writeFile(CmsObject cms, CmsSecurityManager securityManager, CmsFile resource)
562    throws CmsException, CmsVfsException, CmsSecurityException {
563
564        if (CmsImageLoader.isEnabled()) {
565            // check if the user has write access and if resource is locked
566            // done here so that no image operations are performed in case no write access is granted
567            securityManager.checkPermissions(
568                cms.getRequestContext(),
569                resource,
570                CmsPermissionSet.ACCESS_WRITE,
571                true,
572                CmsResourceFilter.ALL);
573
574            // get the downscaler to use
575            CmsImageScaler downScaler = getDownScaler(cms, resource.getRootPath());
576            // create a new image scale adjuster
577            CmsImageAdjuster adjuster = new CmsImageAdjuster(
578                resource.getContents(),
579                resource.getRootPath(),
580                null,
581                downScaler);
582            // update the image scale adjuster - this will calculate the image dimensions and (optionally) adjust the size
583            adjuster.adjust();
584            // continue with the updated content
585            resource.setContents(adjuster.getContent());
586            if (adjuster.getProperties() != null) {
587                // write properties
588                writePropertyObjects(cms, securityManager, resource, adjuster.getProperties());
589            }
590        }
591        return super.writeFile(cms, securityManager, resource);
592    }
593
594    /**
595     * Tries to use the viewbox from the SVG data to determine the image size and add it to the list of properties.<p>
596     *
597     * @param content the content bytes of an SVG file
598     * @param properties the original properties (this object will not be modified)
599     *
600     * @return the amended properties
601     */
602    protected List<CmsProperty> tryAddImageSizeFromSvg(byte[] content, List<CmsProperty> properties) {
603
604        if ((content == null) || (content.length == 0)) {
605            return properties;
606        }
607        List<CmsProperty> newProps = properties;
608        try {
609            double w = -1, h = -1;
610            SAXReader reader = new SAXReader();
611            Document doc = reader.read(new ByteArrayInputStream(content));
612            Element node = (Element)(doc.selectSingleNode("/svg"));
613            if (node != null) {
614                String widthStr = node.attributeValue("width");
615                String heightStr = node.attributeValue("height");
616                SvgSize width = SvgSize.parse(widthStr);
617                SvgSize height = SvgSize.parse(heightStr);
618                if ((width != null) && (height != null) && Objects.equals(width.getUnit(), height.getUnit())) {
619                    // If width and height are given and have the same units, just interpret them as pixels, otherwise use viewbox
620                    w = width.getSize();
621                    h = height.getSize();
622                } else {
623                    String viewboxStr = node.attributeValue("viewBox");
624                    if (viewboxStr != null) {
625                        viewboxStr = viewboxStr.replace(",", " ");
626                        String[] viewboxParts = viewboxStr.trim().split(" +");
627                        if (viewboxParts.length == 4) {
628                            w = Double.parseDouble(viewboxParts[2]);
629                            h = Double.parseDouble(viewboxParts[3]);
630                        }
631                    }
632                }
633                if ((w > 0) && (h > 0)) {
634                    String propValue = "w:" + (int)Math.round(w) + ",h:" + (int)Math.round(h);
635                    Map<String, CmsProperty> propsMap = properties == null
636                    ? new HashMap<>()
637                    : CmsProperty.toObjectMap(properties);
638                    propsMap.put(
639                        CmsPropertyDefinition.PROPERTY_IMAGE_SIZE,
640                        new CmsProperty(CmsPropertyDefinition.PROPERTY_IMAGE_SIZE, null, propValue));
641                    newProps = new ArrayList<>(propsMap.values());
642                }
643
644            }
645        } catch (Exception e) {
646            LOG.error("Error while trying to determine size of SVG: " + e.getLocalizedMessage(), e);
647        }
648        return newProps;
649    }
650
651}