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