1<?php
2/**
3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15
16use DOMAttr;
17use DOMElement;
18use DOMNode;
19
20
21class SvgDocument extends XmlDocument
22{
23
24
25    const CANONICAL = "svg";
26
27    /**
28     * Namespace (used to query with xpath only the svg node)
29     */
30    const SVG_NAMESPACE_PREFIX = "svg";
31    const CONF_SVG_OPTIMIZATION_ENABLE = "svgOptimizationEnable";
32
33    /**
34     * Optimization Configuration
35     */
36    const CONF_OPTIMIZATION_NAMESPACES_TO_KEEP = "svgOptimizationNamespacesToKeep";
37    const CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE = "svgOptimizationAttributesToDelete";
38    const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY = "svgOptimizationElementsToDeleteIfEmpty";
39    const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE = "svgOptimizationElementsToDelete";
40
41    /**
42     * The namespace of the editors
43     * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1841
44     */
45    const EDITOR_NAMESPACE = [
46        'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
47        'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd',
48        'http://www.inkscape.org/namespaces/inkscape',
49        'http://www.bohemiancoding.com/sketch/ns',
50        'http://ns.adobe.com/AdobeIllustrator/10.0/',
51        'http://ns.adobe.com/Graphs/1.0/',
52        'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
53        'http://ns.adobe.com/Variables/1.0/',
54        'http://ns.adobe.com/SaveForWeb/1.0/',
55        'http://ns.adobe.com/Extensibility/1.0/',
56        'http://ns.adobe.com/Flows/1.0/',
57        'http://ns.adobe.com/ImageReplacement/1.0/',
58        'http://ns.adobe.com/GenericCustomNamespace/1.0/',
59        'http://ns.adobe.com/XPath/1.0/',
60        'http://schemas.microsoft.com/visio/2003/SVGExtensions/',
61        'http://taptrix.com/vectorillustrator/svg_extensions',
62        'http://www.figma.com/figma/ns',
63        'http://purl.org/dc/elements/1.1/',
64        'http://creativecommons.org/ns#',
65        'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
66        'http://www.serif.com/',
67        'http://www.vector.evaxdesign.sk',
68    ];
69
70    /**
71     * Default SVG values
72     * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1579
73     * The key are exact (not lowercase) to be able to look them up
74     * for optimization
75     */
76    const SVG_DEFAULT_ATTRIBUTES_VALUE = array(
77        "x" => '0',
78        "y" => '0',
79        "width" => '100%',
80        "height" => '100%',
81        "preserveAspectRatio" => 'xMidYMid meet',
82        "zoomAndPan" => 'magnify',
83        "version" => '1.1',
84        "baseProfile" => 'none',
85        "contentScriptType" => 'application/ecmascript',
86        "contentStyleType" => 'text/css',
87    );
88    const CONF_PRESERVE_ASPECT_RATIO_DEFAULT = "svgPreserveAspectRatioDefault";
89    const SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
90
91
92    /**
93     * Type of svg
94     *   * Icon and tile have the same characteristic (ie viewbox = 0 0 A A) and the color can be set)
95     *   * An illustration does not have rectangle shape and the color is not set
96     */
97    const ICON_TYPE = "icon";
98    const TILE_TYPE = "tile";
99    const ILLUSTRATION_TYPE = "illustration";
100
101    /**
102     * There is only two type of svg icon / tile
103     *   * fill color is on the surface (known also as Solid)
104     *   * stroke, the color is on the path (known as Outline
105     */
106    const COLOR_TYPE_FILL_SOLID = "fill";
107    const COLOR_TYPE_STROKE_OUTLINE = self::STROKE_ATTRIBUTE;
108    const DEFAULT_ICON_WIDTH = "24";
109
110    const CURRENT_COLOR = "currentColor";
111    const VIEW_BOX = "viewBox";
112    const PRESERVE_ATTRIBUTE = "preserve";
113    const STROKE_ATTRIBUTE = "stroke";
114
115    /**
116     * @var string - a name identifier that is added in the SVG
117     */
118    private $name;
119
120    /**
121     * @var boolean do the svg should be optimized
122     */
123    private $shouldBeOptimized;
124    /**
125     * @var Path
126     */
127    private $path;
128
129
130    public function __construct($text)
131    {
132        parent::__construct($text);
133        $this->shouldBeOptimized = PluginUtility::getConfValue(self::CONF_SVG_OPTIMIZATION_ENABLE, 1);
134
135    }
136
137    /**
138     * @param Path $path
139     * @return SvgDocument
140     * @throws ExceptionCombo - if the file does not exist or is not valid
141     *
142     */
143    public static function createSvgDocumentFromPath(Path $path): SvgDocument
144    {
145        if (!FileSystems::exists($path)) {
146            throw new ExceptionCombo("The path ($path) does not exist. A svg document cannot be created", self::CANONICAL);
147        }
148        $text = FileSystems::getContent($path);
149        $svg = new SvgDocument($text);
150        $svg->setName($path->getLastNameWithoutExtension());
151        $svg->setPath($path);
152        return $svg;
153    }
154
155    /**
156     * @throws ExceptionCombo
157     */
158    public static function createSvgDocumentFromMarkup($markup): SvgDocument
159    {
160        return new SvgDocument($markup);
161    }
162
163    private static function preserveStyle(TagAttributes $tagAttributes): bool
164    {
165        $preserve = $tagAttributes->getValue(self::PRESERVE_ATTRIBUTE);
166        if ($preserve !== null) {
167            if (strpos(strtolower($preserve), "style") !== false) {
168                return true;
169            }
170        }
171        return false;
172    }
173
174    /**
175     * @param TagAttributes|null $tagAttributes
176     * @return string
177     *
178     * TODO: What strange is that this is a XML document that is also an image
179     *   This class should be merged with {@link ImageSvg}
180     *   Because we use only {@link Image} function that are here not available because we loose the fact that this is an image
181     *   For instance {@link Image::getCroppingDimensionsWithRatio()}
182     * @throws ExceptionCombo
183     */
184    public function getXmlText(TagAttributes $tagAttributes = null): string
185    {
186
187        if ($tagAttributes === null) {
188            $localTagAttributes = TagAttributes::createEmpty();
189        } else {
190            $localTagAttributes = TagAttributes::createFromTagAttributes($tagAttributes);
191        }
192
193        /**
194         * ViewBox should exist
195         */
196        $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX);
197        if ($viewBox === "") {
198            $width = $this->getXmlDom()->documentElement->getAttribute("width");
199            if ($width === "") {
200                LogUtility::msg("Svg processing stopped. Bad svg: We can't determine the width of the svg ($this) (The viewBox and the width does not exist) ", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
201                return parent::getXmlText();
202            }
203            $height = $this->getXmlDom()->documentElement->getAttribute("height");
204            if ($height === "") {
205                LogUtility::msg("Svg processing stopped. Bad svg: We can't determine the height of the svg ($this) (The viewBox and the height does not exist) ", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
206                return parent::getXmlText();
207            }
208            $this->getXmlDom()->documentElement->setAttribute(self::VIEW_BOX, "0 0 $width $height");
209        }
210
211        if ($this->shouldOptimize()) {
212            $this->optimize($localTagAttributes);
213        }
214
215        // Set the name (icon) attribute for test selection
216        if ($localTagAttributes->hasComponentAttribute("name")) {
217            $name = $localTagAttributes->getValueAndRemove("name");
218            $this->setRootAttribute('data-name', $name);
219        }
220
221        // Handy variable
222        $documentElement = $this->getXmlDom()->documentElement;
223
224        // With requested
225        $requestedWidth = $localTagAttributes->getValueAndRemove(Dimension::WIDTH_KEY);
226
227        $svgUsageType = $localTagAttributes->getValue(TagAttributes::TYPE_KEY);
228
229        /**
230         * Svg Structure
231         *
232         * All attributes that are applied for all usage (output independent)
233         * and that depends only on the structure of the icon
234         *
235         * Why ? Because {@link \syntax_plugin_combo_pageimage}
236         * can be an icon or an illustrative image
237         *
238         */
239        try {
240            $mediaWidth = $this->getMediaWidth();
241        } catch (ExceptionCombo $e) {
242            LogUtility::msg("The media width of ($this) returns the following error ({$e->getMessage()}). The processing was stopped");
243            return parent::getXmlText();
244        }
245        try {
246            $mediaHeight = $this->getMediaHeight();
247        } catch (ExceptionCombo $e) {
248            LogUtility::msg("The media height of ($this) returns the following error ({$e->getMessage()}). The processing was stopped");
249            return parent::getXmlText();
250        }
251        if ($mediaWidth !== null
252            && $mediaHeight !== null
253            && $mediaWidth == $mediaHeight
254            && $mediaWidth < 400) // 356 for logos telegram are the size of the twitter emoji but tile may be bigger ?
255        {
256            $svgStructureType = self::ICON_TYPE;
257        } else {
258            $svgStructureType = self::ILLUSTRATION_TYPE;
259
260            // some icon may be bigger
261            // in size than 400. example 1024 for ant-design:table-outlined
262            // https://github.com/ant-design/ant-design-icons/blob/master/packages/icons-svg/svg/outlined/table.svg
263            // or not squared
264            // if the usage is determined or the svg is in the icon directory, it just takes over.
265            if ($svgUsageType === self::ICON_TYPE || $this->isInIconDirectory()) {
266                $svgStructureType = self::ICON_TYPE;
267            }
268
269        }
270
271        /**
272         * Svg type
273         * The svg type is the svg usage
274         * How the svg should be shown (the usage)
275         *
276         * We need it to make the difference between an icon
277         *   * in a paragraph (the width and height are the same)
278         *   * as an illustration in a page image (the width and height may be not the same)
279         */
280        if ($svgUsageType === null) {
281            switch ($svgStructureType) {
282                case self::ICON_TYPE:
283                    $svgUsageType = self::ICON_TYPE;
284                    break;
285                default:
286                    $svgUsageType = self::ILLUSTRATION_TYPE;
287                    break;
288            }
289        }
290        switch ($svgUsageType) {
291            case self::ICON_TYPE:
292            case self::TILE_TYPE:
293                /**
294                 * Dimension
295                 *
296                 * Using a icon in the navbrand component of bootstrap
297                 * require the set of width and height otherwise
298                 * the svg has a calculated width of null
299                 * and the bar component are below the brand text
300                 *
301                 */
302                $appliedWidth = $requestedWidth;
303                if ($requestedWidth === null) {
304                    if ($svgUsageType == self::ICON_TYPE) {
305                        $appliedWidth = self::DEFAULT_ICON_WIDTH;
306                    } else {
307                        // tile
308                        $appliedWidth = "192";
309                    }
310                }
311                /**
312                 * Dimension
313                 * The default unit on attribute is pixel, no need to add it
314                 * as in CSS
315                 */
316                $localTagAttributes->addOutputAttributeValue("width", $appliedWidth);
317                $height = $localTagAttributes->getValueAndRemove(Dimension::HEIGHT_KEY, $appliedWidth);
318                $localTagAttributes->addOutputAttributeValue("height", $height);
319                break;
320            default:
321                /**
322                 * Illustration / Image
323                 */
324                /**
325                 * Responsive SVG
326                 */
327                if (!$localTagAttributes->hasComponentAttribute("preserveAspectRatio")) {
328                    /**
329                     *
330                     * Keep the same height
331                     * Image in the Middle and border deleted when resizing
332                     * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
333                     * Default is xMidYMid meet
334                     */
335                    $defaultAspectRatio = PluginUtility::getConfValue(self::CONF_PRESERVE_ASPECT_RATIO_DEFAULT, "xMidYMid slice");
336                    $localTagAttributes->addOutputAttributeValue("preserveAspectRatio", $defaultAspectRatio);
337                }
338
339                /**
340                 * Note on dimension width and height
341                 * Width and height element attribute are in reality css style properties.
342                 *   ie the max-width style
343                 * They are treated in {@link PluginUtility::processStyle()}
344                 */
345
346                /**
347                 * Adapt to the container by default
348                 * Height `auto` and not `100%` otherwise you get a layout shift
349                 */
350                $localTagAttributes->addStyleDeclarationIfNotSet("width", "100%");
351                $localTagAttributes->addStyleDeclarationIfNotSet("height", "auto");
352
353
354                if ($requestedWidth !== null) {
355
356                    /**
357                     * If a dimension was set, it's seen by default as a max-width
358                     * If it should not such as in a card, this property is already set
359                     * and is not overwritten
360                     */
361                    try {
362                        $widthInPixel = Dimension::toPixelValue($requestedWidth);
363                    } catch (ExceptionCombo $e) {
364                        LogUtility::msg("The requested width $requestedWidth could not be converted to pixel. It returns the following error ({$e->getMessage()}). Processing was stopped");
365                        return parent::getXmlText();
366                    }
367                    $localTagAttributes->addStyleDeclarationIfNotSet("max-width", "{$widthInPixel}px");
368
369                    /**
370                     * To have an internal width
371                     * and not shrink on the css property `width: auto !important;`
372                     * of a table
373                     */
374                    $this->setRootAttribute("width", $widthInPixel);
375
376                }
377
378                break;
379        }
380
381
382        switch ($svgStructureType) {
383            case self::ICON_TYPE:
384            case self::TILE_TYPE:
385                /**
386                 * Determine if this is a:
387                 *   * fill one color
388                 *   * fill two colors
389                 *   * or stroke svg icon
390                 *
391                 * The color can be set:
392                 *   * on fill (surface)
393                 *   * on stroke (line)
394                 *
395                 * If the stroke attribute is not present this is a fill icon
396                 */
397                $svgColorType = self::COLOR_TYPE_FILL_SOLID;
398                if ($documentElement->hasAttribute(self::STROKE_ATTRIBUTE)) {
399                    $svgColorType = self::COLOR_TYPE_STROKE_OUTLINE;
400                }
401                /**
402                 * Double color icon ?
403                 */
404                $isDoubleColor = false;
405                if ($svgColorType === self::COLOR_TYPE_FILL_SOLID) {
406                    $svgFillsElement = $this->xpath("//*[@fill]");
407                    $fillColors = [];
408                    for ($i = 0; $i < $svgFillsElement->length; $i++) {
409                        /**
410                         * @var DOMElement $nodeElement
411                         */
412                        $nodeElement = $svgFillsElement[$i];
413                        $value = $nodeElement->getAttribute("fill");
414                        if ($value !== "none") {
415                            /**
416                             * Icon may have none alongside colors
417                             * Example:
418                             */
419                            $fillColors[$value] = $value;
420                        }
421                    }
422                    if (sizeof($fillColors) > 1) {
423                        $isDoubleColor = true;
424                    }
425                }
426
427                /**
428                 * CurrentColor
429                 *
430                 * By default, the icon should have this property when downloaded
431                 * but if this not the case (such as for Material design), we set them
432                 *
433                 * Feather set it on the stroke
434                 * Example: view-source:https://raw.githubusercontent.com/feathericons/feather/master/icons/airplay.svg
435                 * <svg
436                 *  fill="none"
437                 *  stroke="currentColor">
438                 */
439                if (!$isDoubleColor && !$documentElement->hasAttribute("fill")) {
440
441                    /**
442                     * Note: if fill was not set, the default color would be black
443                     */
444                    $localTagAttributes->addOutputAttributeValue("fill", self::CURRENT_COLOR);
445
446                }
447
448
449                /**
450                 * Eva/Carbon Source Icon are not optimized at the source
451                 * Example:
452                 *   * eva:facebook-fill
453                 *   * carbon:logo-tumblr (https://github.com/carbon-design-system/carbon/issues/5568)
454                 *
455                 * We delete the rectangle
456                 * Style should have already been deleted by the optimization
457                 *
458                 * This optimization should happen if the color is set
459                 * or not because we set the color value to `currentColor`
460                 *
461                 * If the rectangle stay, we just see a black rectangle
462                 */
463                if ($this->path !== null) {
464                    $pathString = $this->path->toAbsolutePath()->toString();
465                    if (
466                        preg_match("/carbon|eva/i", $pathString) === 1
467                    ) {
468                        $this->deleteAllElements("rect");
469                    }
470                }
471
472                $color = $localTagAttributes->getValueAndRemoveIfPresent(ColorRgb::COLOR);
473                if ($svgUsageType === self::ILLUSTRATION_TYPE && $color === null) {
474                    $primaryColor = Site::getPrimaryColorValue();
475                    if ($primaryColor !== null) {
476                        $color = $primaryColor;
477                    }
478                }
479
480                /**
481                 * Color
482                 * Color applies only if this is an icon.
483                 *
484                 */
485                if ($color !== null) {
486                    /**
487                     *
488                     * We say that this is used only for an icon (<72 px)
489                     *
490                     * Not that an icon svg file can also be used as {@link \syntax_plugin_combo_pageimage}
491                     *
492                     * We don't set it as a styling attribute
493                     * because it's not taken into account if the
494                     * svg is used as a background image
495                     * fill or stroke should have at minimum "currentColor"
496                     */
497                    $colorValue = ColorRgb::createFromString($color)->toCssValue();
498
499
500                    switch ($svgColorType) {
501                        case self::COLOR_TYPE_FILL_SOLID:
502
503
504                            if (!$isDoubleColor) {
505
506                                $localTagAttributes->addOutputAttributeValue("fill", $colorValue);
507
508                                if ($colorValue !== self::CURRENT_COLOR) {
509                                    /**
510                                     * Update the fill property on sub-path
511                                     * If the fill is set on sub-path, it will not work
512                                     *
513                                     * fill may be set on group or whatever
514                                     */
515                                    $svgPaths = $this->xpath("//*[local-name()='path' or local-name()='g']");
516                                    for ($i = 0; $i < $svgPaths->length; $i++) {
517                                        /**
518                                         * @var DOMElement $nodeElement
519                                         */
520                                        $nodeElement = $svgPaths[$i];
521                                        $value = $nodeElement->getAttribute("fill");
522                                        if ($value !== "none") {
523                                            if ($nodeElement->parentNode->tagName !== "svg") {
524                                                $nodeElement->setAttribute("fill", self::CURRENT_COLOR);
525                                            } else {
526                                                $this->removeAttributeValue("fill", $nodeElement);
527                                            }
528                                        }
529                                    }
530
531                                }
532                            } else {
533                                // double color
534                                $firsFillElement = $this->xpath("//*[@fill][1]")->item(0);
535                                if ($firsFillElement instanceof DOMElement) {
536                                    $firsFillElement->setAttribute("fill", $colorValue);
537                                }
538                            }
539
540                            break;
541                        case self::COLOR_TYPE_STROKE_OUTLINE:
542                            $localTagAttributes->addOutputAttributeValue("fill", "none");
543                            $localTagAttributes->addOutputAttributeValue(self::STROKE_ATTRIBUTE, $colorValue);
544
545                            if ($colorValue !== self::CURRENT_COLOR) {
546                                /**
547                                 * Delete the stroke property on sub-path
548                                 */
549                                // if the fill is set on sub-path, it will not work
550                                $svgPaths = $this->xpath("//*[local-name()='path']");
551                                for ($i = 0; $i < $svgPaths->length; $i++) {
552                                    /**
553                                     * @var DOMElement $nodeElement
554                                     */
555                                    $nodeElement = $svgPaths[$i];
556                                    $value = $nodeElement->getAttribute(self::STROKE_ATTRIBUTE);
557                                    if ($value !== "none") {
558                                        $this->removeAttributeValue(self::STROKE_ATTRIBUTE, $nodeElement);
559                                    } else {
560                                        $this->removeNode($nodeElement);
561                                    }
562                                }
563
564                            }
565                            break;
566                    }
567
568                }
569
570
571                break;
572
573        }
574
575        /**
576         * Ratio / Cropping (used for ratio cropping)
577         * Width and height used to set the viewBox of a svg
578         * to crop it
579         * (In a raster image, there is not this distinction)
580         *
581         * With an icon, the viewBox can be small but it can be zoomed out
582         * via the {@link Dimension::WIDTH_KEY}
583         */
584        $processedWidth = $mediaWidth;
585        $processedHeight = $mediaHeight;
586        if ($localTagAttributes->hasComponentAttribute(Dimension::RATIO_ATTRIBUTE)) {
587            // We get a crop, it means that we need to change the viewBox
588            $ratio = $localTagAttributes->getValueAndRemoveIfPresent(Dimension::RATIO_ATTRIBUTE);
589            try {
590                $targetRatio = Dimension::convertTextualRatioToNumber($ratio);
591            } catch (ExceptionCombo $e) {
592                LogUtility::msg("The target ratio attribute ($ratio) returns the following error ({$e->getMessage()}). The svg processing was stopped");
593                return parent::getXmlText();
594            }
595            [$processedWidth, $processedHeight] = Image::getCroppingDimensionsWithRatio($targetRatio, $mediaWidth, $mediaHeight);
596
597            $this->setRootAttribute(self::VIEW_BOX, "0 0 $processedWidth $processedHeight");
598
599        }
600
601        /**
602         * Zoom occurs after the crop if any
603         */
604        $zoomFactor = $localTagAttributes->getValueAsInteger(Dimension::ZOOM_ATTRIBUTE);
605        if ($zoomFactor === null
606            && $svgStructureType === self::ICON_TYPE
607            && $svgUsageType === self::ILLUSTRATION_TYPE
608        ) {
609            $zoomFactor = -4;
610        }
611        if ($zoomFactor !== null) {
612            // icon case, we zoom out otherwise, this is ugly, the icon takes the whole place
613            if ($zoomFactor < 0) {
614                $processedWidth = -$zoomFactor * $processedWidth;
615                $processedHeight = -$zoomFactor * $processedHeight;
616            } else {
617                $processedWidth = $processedWidth / $zoomFactor;
618                $processedHeight = $processedHeight / $zoomFactor;
619            }
620            // center
621            $actualWidth = $mediaWidth;
622            $actualHeight = $mediaHeight;
623            $x = -($processedWidth - $actualWidth) / 2;
624            $y = -($processedHeight - $actualHeight) / 2;
625            $this->setRootAttribute(self::VIEW_BOX, "$x $y $processedWidth $processedHeight");
626        }
627
628
629        // Add a class on each path for easy styling
630        if (!empty($this->name)) {
631            $svgPaths = $this->xpath("//*[local-name()='path']");
632            for ($i = 0;
633                 $i < $svgPaths->length;
634                 $i++) {
635
636                $stylingClass = $this->name . "-" . $i;
637                $this->addAttributeValue("class", $stylingClass, $svgPaths[$i]);
638
639            }
640        }
641
642        /**
643         * Svg attribute are case sensitive
644         * but not the component attribute key
645         * we get the value and set it then as HTML to have the good casing
646         * on this attribute
647         */
648        $caseSensitives = ["preserveAspectRatio"];
649        foreach ($caseSensitives as $caseSensitive) {
650            if ($localTagAttributes->hasComponentAttribute($caseSensitive)) {
651                $aspectRatio = $localTagAttributes->getValueAndRemove($caseSensitive);
652                $localTagAttributes->addOutputAttributeValue($caseSensitive, $aspectRatio);
653            }
654        }
655
656        /**
657         * Old model where the src was parsed in the render
658         * When the attributes are in the {@link Path} we can delete this
659         */
660        $localTagAttributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME);
661
662        /**
663         * Set the attributes to the root
664         */
665        $toHtmlArray = $localTagAttributes->toHtmlArray();
666        foreach ($toHtmlArray as $name => $value) {
667            if (in_array($name, TagAttributes::MULTIPLE_VALUES_ATTRIBUTES)) {
668                $actualValue = $this->getRootAttributeValue($name);
669                if ($actualValue !== null) {
670                    $value = TagAttributes::mergeClassNames($value, $actualValue);
671                }
672            }
673            $this->setRootAttribute($name, $value);
674        }
675
676        return parent::getXmlText();
677
678    }
679
680
681    /**
682     * @param $boolean
683     * @return SvgDocument
684     */
685    public
686    function setShouldBeOptimized($boolean): SvgDocument
687    {
688        $this->shouldBeOptimized = $boolean;
689        return $this;
690    }
691
692    /**
693     * @throws ExceptionCombo
694     */
695    public
696    function getMediaWidth(): int
697    {
698        $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX);
699        if ($viewBox !== "") {
700            $attributes = $this->getViewBoxAttributes($viewBox);
701            $viewBoxWidth = $attributes[2];
702            try {
703                return DataType::toInteger($viewBoxWidth);
704            } catch (ExceptionCombo $e) {
705                throw new ExceptionCombo("The media with ($viewBoxWidth) of the svg image ($this) is not a valid integer value");
706            }
707        }
708
709        /**
710         * Case with some icon such as
711         * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg
712         */
713        $width = $this->getXmlDom()->documentElement->getAttribute("width");
714        if ($width === "") {
715            throw new ExceptionCombo("The svg ($this) does not have a viewBox or width attribute, the intrinsic width cannot be determined");
716        }
717        try {
718            return DataType::toInteger($width);
719        } catch (ExceptionCombo $e) {
720            throw new ExceptionCombo("The media width ($width) of the svg image ($this) is not a valid integer value");
721        }
722
723    }
724
725    /**
726     * @throws ExceptionCombo
727     */
728    public
729    function getMediaHeight(): int
730    {
731        $viewBox = $this->getXmlDom()->documentElement->getAttribute(self::VIEW_BOX);
732        if ($viewBox !== "") {
733            $attributes = $this->getViewBoxAttributes($viewBox);
734            $viewBoxHeight = $attributes[3];
735            try {
736                return DataType::toInteger($viewBoxHeight);
737            } catch (ExceptionCombo $e) {
738                throw new ExceptionCombo("The media height of the svg image ($this) is not a valid integer value");
739            }
740        }
741        /**
742         * Case with some icon such as
743         * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg
744         */
745        $height = $this->getXmlDom()->documentElement->getAttribute("height");
746        if ($height === "") {
747            throw new ExceptionCombo("The svg ($this) does not have a viewBox or height attribute, the intrinsic height cannot be determined");
748        }
749        try {
750            return DataType::toInteger($height);
751        } catch (ExceptionCombo $e) {
752            throw new ExceptionCombo("The media width ($height) of the svg image ($this) is not a valid integer value");
753        }
754
755    }
756
757
758    private
759    function getSvgPaths()
760    {
761        if ($this->isXmlExtensionLoaded()) {
762
763            /**
764             * If the file was optimized, the svg namespace
765             * does not exist anymore
766             */
767            $namespace = $this->getDocNamespaces();
768            if (isset($namespace[self::SVG_NAMESPACE_PREFIX])) {
769                $svgNamespace = self::SVG_NAMESPACE_PREFIX;
770                $query = "//$svgNamespace:path";
771            } else {
772                $query = "//*[local-name()='path']";
773            }
774            return $this->xpath($query);
775        } else {
776            return array();
777        }
778
779
780    }
781
782
783    /**
784     * Optimization
785     * Based on https://jakearchibald.github.io/svgomg/
786     * (gui of https://github.com/svg/svgo)
787     */
788    public
789    function optimize($tagAttributes)
790    {
791
792        if ($this->shouldOptimize()) {
793
794            /**
795             * Delete Editor namespace
796             * https://github.com/svg/svgo/blob/master/plugins/removeEditorsNSData.js
797             */
798            $confNamespaceToKeeps = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_NAMESPACES_TO_KEEP);
799            $namespaceToKeep = StringUtility::explodeAndTrim($confNamespaceToKeeps, ",");
800            foreach ($this->getDocNamespaces() as $namespacePrefix => $namespaceUri) {
801                if (
802                    !empty($namespacePrefix)
803                    && $namespacePrefix != "svg"
804                    && !in_array($namespacePrefix, $namespaceToKeep)
805                    && in_array($namespaceUri, self::EDITOR_NAMESPACE)
806                ) {
807                    $this->removeNamespace($namespaceUri);
808                }
809            }
810
811            /**
812             * Delete empty namespace rules
813             */
814            $documentElement = &$this->getXmlDom()->documentElement;
815            foreach ($this->getDocNamespaces() as $namespacePrefix => $namespaceUri) {
816                $nodes = $this->xpath("//*[namespace-uri()='$namespaceUri']");
817                $attributes = $this->xpath("//@*[namespace-uri()='$namespaceUri']");
818                if ($nodes->length == 0 && $attributes->length == 0) {
819                    $result = $documentElement->removeAttributeNS($namespaceUri, $namespacePrefix);
820                    if ($result === false) {
821                        LogUtility::msg("Internal error: The deletion of the empty namespace ($namespacePrefix:$namespaceUri) didn't succeed", LogUtility::LVL_MSG_WARNING, "support");
822                    }
823                }
824            }
825
826            /**
827             * Delete comments
828             */
829            $commentNodes = $this->xpath("//comment()");
830            foreach ($commentNodes as $commentNode) {
831                $this->removeNode($commentNode);
832            }
833
834            /**
835             * Delete default value (version=1.1 for instance)
836             */
837            $defaultValues = self::SVG_DEFAULT_ATTRIBUTES_VALUE;
838            foreach ($documentElement->attributes as $attribute) {
839                /** @var DOMAttr $attribute */
840                $name = $attribute->name;
841                if (isset($defaultValues[$name])) {
842                    if ($defaultValues[$name] == $attribute->value) {
843                        $documentElement->removeAttributeNode($attribute);
844                    }
845                }
846            }
847
848            /**
849             * Suppress the attributes (by default id, style and class, data-name)
850             */
851            $attributeConfToDelete = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE, "id, style, class, data-name");
852            $attributesNameToDelete = StringUtility::explodeAndTrim($attributeConfToDelete, ",");
853            foreach ($attributesNameToDelete as $value) {
854
855                if (in_array($value, ["style", "class", "id"]) && self::preserveStyle($tagAttributes)) {
856                    // we preserve the style, we preserve the class
857                    continue;
858                }
859
860                $nodes = $this->xpath("//@$value");
861                foreach ($nodes as $node) {
862                    /** @var DOMAttr $node */
863                    /** @var DOMElement $DOMNode */
864                    $DOMNode = $node->parentNode;
865                    $DOMNode->removeAttributeNode($node);
866                }
867            }
868
869            /**
870             * Remove width/height that coincides with a viewBox attr
871             * https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute
872             * Example:
873             * <svg width="100" height="50" viewBox="0 0 100 50">
874             * <svg viewBox="0 0 100 50">
875             *
876             */
877            $widthAttributeValue = $documentElement->getAttribute("width");
878            if (!empty($widthAttributeValue)) {
879                $widthPixel = Unit::toPixel($widthAttributeValue);
880
881                $heightAttributeValue = $documentElement->getAttribute("height");
882                if (!empty($heightAttributeValue)) {
883                    $heightPixel = Unit::toPixel($heightAttributeValue);
884
885                    // ViewBox
886                    $viewBoxAttribute = $documentElement->getAttribute(self::VIEW_BOX);
887                    if (!empty($viewBoxAttribute)) {
888                        $viewBoxAttributeAsArray = StringUtility::explodeAndTrim($viewBoxAttribute, " ");
889
890                        if (sizeof($viewBoxAttributeAsArray) == 4) {
891                            $minX = $viewBoxAttributeAsArray[0];
892                            $minY = $viewBoxAttributeAsArray[1];
893                            $widthViewPort = $viewBoxAttributeAsArray[2];
894                            $heightViewPort = $viewBoxAttributeAsArray[3];
895                            if (
896                                $minX == 0 &
897                                $minY == 0 &
898                                $widthViewPort == $widthPixel &
899                                $heightViewPort == $heightPixel
900                            ) {
901                                $documentElement->removeAttribute("width");
902                                $documentElement->removeAttribute("height");
903                            }
904
905                        }
906                    }
907                }
908            }
909
910
911            /**
912             * Suppress script and style
913             *
914             *
915             * Delete of scripts https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
916             *
917             * And defs/style
918             *
919             * The style can leak in other icon/svg inlined in the document
920             *
921             * Technically on icon, there should be no `style`
922             * on inline icon otherwise, the css style can leak
923             *
924             * Example with carbon that use cls-1 on all icons
925             * https://github.com/carbon-design-system/carbon/issues/5568
926             * The facebook icon has a class cls-1 with an opacity of 0
927             * that leaks to the tumblr icon that has also a cls-1 class
928             *
929             * The illustration uses inline fill to color and styled
930             * For instance, all un-draw: https://undraw.co/illustrations
931             */
932            $elementsToDeleteConf = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE, "script, style, title, desc");
933            $elementsToDelete = StringUtility::explodeAndTrim($elementsToDeleteConf, ",");
934            foreach ($elementsToDelete as $elementToDelete) {
935                if ($elementToDelete === "style" && self::preserveStyle($tagAttributes)) {
936                    continue;
937                }
938                $this->deleteAllElements($elementToDelete);
939            }
940
941            // Delete If Empty
942            //   * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
943            //   * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata
944            $elementsToDeleteIfEmptyConf = PluginUtility::getConfValue(self::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY, "metadata, defs, g");
945            $elementsToDeleteIfEmpty = StringUtility::explodeAndTrim($elementsToDeleteIfEmptyConf);
946            foreach ($elementsToDeleteIfEmpty as $elementToDeleteIfEmpty) {
947                $elementNodeList = $this->xpath("//*[local-name()='$elementToDeleteIfEmpty']");
948                foreach ($elementNodeList as $element) {
949                    /** @var DOMElement $element */
950                    if (!$element->hasChildNodes()) {
951                        $element->parentNode->removeChild($element);
952                    }
953                }
954            }
955
956            /**
957             * Delete the svg prefix namespace definition
958             * At the end to be able to query with svg as prefix
959             */
960            if (!in_array("svg", $namespaceToKeep)) {
961                $documentElement->removeAttributeNS(self::SVG_NAMESPACE_URI, self::SVG_NAMESPACE_PREFIX);
962            }
963
964        }
965    }
966
967    public
968    function shouldOptimize()
969    {
970
971        return $this->shouldBeOptimized;
972
973    }
974
975    /**
976     * The name is used to add class in the svg
977     * @param $name
978     */
979    private
980    function setName($name)
981    {
982        $this->name = $name;
983    }
984
985    /**
986     * Set the context
987     * @param Path $path
988     */
989    private
990    function setPath(Path $path)
991    {
992        $this->path = $path;
993    }
994
995    public
996    function __toString()
997    {
998        if ($this->path !== null) {
999            return $this->path->__toString();
1000        }
1001        if ($this->name !== null) {
1002            return $this->name;
1003        }
1004        return "unknown";
1005    }
1006
1007    private
1008    function isInIconDirectory(): bool
1009    {
1010        if ($this->path == null) {
1011            return false;
1012        }
1013        $iconNameSpace = PluginUtility::getConfValue(Icon::CONF_ICONS_MEDIA_NAMESPACE, Icon::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT);
1014        if (strpos($this->path->toString(), $iconNameSpace) !== false) {
1015            return true;
1016        }
1017        return false;
1018    }
1019
1020    /**
1021     * An utility function to know how to remove a node
1022     * @param DOMNode $nodeElement
1023     */
1024    private
1025    function removeNode(DOMNode $nodeElement)
1026    {
1027        $nodeElement->parentNode->removeChild($nodeElement);
1028    }
1029
1030    private
1031    function deleteAllElements(string $elementName)
1032    {
1033        $svgElement = $this->xpath("//*[local-name()='$elementName']");
1034        for ($i = 0; $i < $svgElement->length; $i++) {
1035            /**
1036             * @var DOMElement $nodeElement
1037             */
1038            $nodeElement = $svgElement[$i];
1039            $this->removeNode($nodeElement);
1040        }
1041    }
1042
1043    /**
1044     * @param string $viewBox
1045     * @return string[]
1046     */
1047    private function getViewBoxAttributes(string $viewBox): array
1048    {
1049        $attributes = explode(" ", $viewBox);
1050        if(sizeof($attributes)===1){
1051            /**
1052             * We may find also comma. Example:
1053             * viewBox="0,0,433.62,289.08"
1054             */
1055            $attributes = explode(",", $viewBox);
1056        }
1057        return $attributes;
1058    }
1059
1060
1061}
1062