1<?php
2
3
4namespace ComboStrap;
5
6use ComboStrap\TagAttribute\StyleAttribute;
7use ComboStrap\Web\Url;
8use ComboStrap\Xml\XmlDocument;
9use ComboStrap\Xml\XmlSystems;
10use DOMAttr;
11use DOMElement;
12use splitbrain\phpcli\Colors;
13
14/**
15 * Class ImageSvg
16 * @package ComboStrap
17 *
18 * Svg image fetch processing that can output:
19 *   * an URL for an HTTP request
20 *   * an SvgFile for an HTTP response or any further processing
21 *
22 * The original svg can be set with:
23 *   * the {@link FetcherSvg::setSourcePath() original path}
24 *   * the {@link FetcherSvg::setRequestedName() name} if this is an {@link FetcherSvg::setRequestedType() icon type}, the original path is then determined on {@link FetcherSvg::getSourcePath() get}
25 *   * or by {@link FetcherSvg::setMarkup() Svg Markup}
26 *
27 */
28class FetcherSvg extends IFetcherLocalImage
29{
30
31    use FetcherTraitWikiPath {
32        setSourcePath as protected setOriginalPathTraitAlias;
33    }
34
35    const EXTENSION = "svg";
36    const CANONICAL = "svg";
37
38    const REQUESTED_PRESERVE_ASPECT_RATIO_KEY = "preserveAspectRatio";
39    public const CURRENT_COLOR = "currentColor";
40    /**
41     * Default SVG values
42     * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1579
43     * The key are exact (not lowercase) to be able to look them up
44     * for optimization
45     */
46    public const SVG_DEFAULT_ATTRIBUTES_VALUE = array(
47        "x" => '0',
48        "y" => '0',
49        "width" => '100%',
50        "height" => '100%',
51        "preserveAspectRatio" => 'xMidYMid meet',
52        "zoomAndPan" => 'magnify',
53        "version" => '1.1',
54        "baseProfile" => 'none',
55        "contentScriptType" => 'application/ecmascript',
56        "contentStyleType" => 'text/css',
57    );
58    /**
59     * The namespace of the editors
60     * https://github.com/svg/svgo/blob/master/plugins/_collections.js#L1841
61     */
62    public const EDITOR_NAMESPACE = [
63        'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
64        'http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd',
65        'http://www.inkscape.org/namespaces/inkscape',
66        'http://www.bohemiancoding.com/sketch/ns',
67        'http://ns.adobe.com/AdobeIllustrator/10.0/',
68        'http://ns.adobe.com/Graphs/1.0/',
69        'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
70        'http://ns.adobe.com/Variables/1.0/',
71        'http://ns.adobe.com/SaveForWeb/1.0/',
72        'http://ns.adobe.com/Extensibility/1.0/',
73        'http://ns.adobe.com/Flows/1.0/',
74        'http://ns.adobe.com/ImageReplacement/1.0/',
75        'http://ns.adobe.com/GenericCustomNamespace/1.0/',
76        'http://ns.adobe.com/XPath/1.0/',
77        'http://schemas.microsoft.com/visio/2003/SVGExtensions/',
78        'http://taptrix.com/vectorillustrator/svg_extensions',
79        'http://www.figma.com/figma/ns',
80        'http://purl.org/dc/elements/1.1/',
81        'http://creativecommons.org/ns#',
82        'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
83        'http://www.serif.com/',
84        'http://www.vector.evaxdesign.sk',
85    ];
86    public const CONF_PRESERVE_ASPECT_RATIO_DEFAULT = "svgPreserveAspectRatioDefault";
87    public const TILE_TYPE = "tile";
88    public const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE = "svgOptimizationElementsToDelete";
89    public const VIEW_BOX = "viewBox";
90    /**
91     * Optimization Configuration
92     */
93    public const CONF_OPTIMIZATION_NAMESPACES_TO_KEEP = "svgOptimizationNamespacesToKeep";
94    public const CONF_SVG_OPTIMIZATION_ENABLE = "svgOptimizationEnable";
95    public const COLOR_TYPE_STROKE_OUTLINE = FetcherSvg::STROKE_ATTRIBUTE;
96    public const CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE = "svgOptimizationAttributesToDelete";
97    public const CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY = "svgOptimizationElementsToDeleteIfEmpty";
98    public const SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
99    public const STROKE_ATTRIBUTE = "stroke";
100    public const DEFAULT_ICON_LENGTH = 24;
101    public const REQUESTED_NAME_ATTRIBUTE = "name";
102    public const REQUESTED_PRESERVE_ATTRIBUTE = "preserve";
103    public const ILLUSTRATION_TYPE = "illustration";
104    /**
105     * There is only two type of svg icon / tile
106     *   * fill color is on the surface (known also as Solid)
107     *   * stroke, the color is on the path (known as Outline
108     */
109    public const COLOR_TYPE_FILL_SOLID = "fill";
110    /**
111     * Type of svg
112     *   * Icon and tile have the same characteristic (ie viewbox = 0 0 A A) and the color can be set)
113     *   * An illustration does not have rectangle shape and the color is not set
114     */
115    public const ICON_TYPE = "icon";
116    /**
117     * Namespace (used to query with xpath only the svg node)
118     */
119    public const SVG_NAMESPACE_PREFIX = "svg";
120    const TAG = "svg";
121    public const NAME_ATTRIBUTE = "name";
122    public const DATA_NAME_HTML_ATTRIBUTE = "data-name";
123    const DEFAULT_TILE_WIDTH = 192;
124
125
126    private ?ColorRgb $color = null;
127    private ?string $preserveAspectRatio = null;
128    private ?bool $preserveStyle = null;
129    private ?string $requestedType = null;
130    private bool $processed = false;
131    private ?float $zoomFactor = null;
132    private ?string $requestedClass = null;
133    private int $intrinsicHeight;
134    private int $intrinsicWidth;
135    private string $name;
136
137
138    private static function createSvgEmpty(): FetcherSvg
139    {
140        return new FetcherSvg();
141    }
142
143    /**
144     */
145    public static function createSvgFromPath(WikiPath $path): FetcherSvg
146    {
147        $fetcher = self::createSvgEmpty();
148
149        $fetcher->setSourcePath($path);
150        return $fetcher;
151    }
152
153    /**
154     * @throws ExceptionBadArgument
155     */
156    public static function createSvgFromFetchUrl(Url $fetchUrl): FetcherSvg
157    {
158        $fetchSvg = self::createSvgEmpty();
159        $fetchSvg->buildFromUrl($fetchUrl);
160        return $fetchSvg;
161    }
162
163    /**
164     * @param string $markup - the svg as a string
165     * @param string $name - a name identifier (used in diff)
166     * @return FetcherSvg
167     * @throws ExceptionBadSyntax
168     */
169    public static function createSvgFromMarkup(string $markup, string $name): FetcherSvg
170    {
171        return self::createSvgEmpty()->setMarkup($markup, $name);
172    }
173
174    /**
175     * @param TagAttributes $tagAttributes
176     * @return FetcherSvg
177     * @throws ExceptionBadArgument
178     * @throws ExceptionBadSyntax
179     * @throws ExceptionCompile
180     */
181    public static function createFromAttributes(TagAttributes $tagAttributes): FetcherSvg
182    {
183        $fetcher = FetcherSvg::createSvgEmpty();
184        $fetcher->buildFromTagAttributes($tagAttributes);
185        return $fetcher;
186    }
187
188    /**
189     * @throws ExceptionNotFound
190     */
191    public function getRequestedOptimization(): bool
192    {
193
194        if ($this->requestedOptimization === null) {
195            throw new ExceptionNotFound("Optimization was not set");
196        }
197        return $this->requestedOptimization;
198
199    }
200
201    public function getRequestedOptimizeOrDefault(): bool
202    {
203        try {
204            return $this->getRequestedOptimization();
205        } catch (ExceptionNotFound $e) {
206            return SiteConfig::getConfValue(FetcherSvg::CONF_SVG_OPTIMIZATION_ENABLE, 1);
207        }
208
209    }
210
211    /**
212     * @throws ExceptionNotFound
213     */
214    public function getRequestedPreserveStyle(): bool
215    {
216
217        if ($this->preserveStyle === null) {
218            throw new ExceptionNotFound("No preserve style attribute was set");
219        }
220        return $this->preserveStyle;
221
222    }
223
224
225    /**
226     * @param $boolean
227     * @return FetcherSvg
228     */
229    public function setRequestedOptimization($boolean): FetcherSvg
230    {
231        $this->requestedOptimization = $boolean;
232        return $this;
233    }
234
235    /**
236     * Optimization
237     * Based on https://jakearchibald.github.io/svgomg/
238     * (gui of https://github.com/svg/svgo)
239     *
240     * @throws ExceptionBadSyntax
241     * @throws ExceptionBadState
242     */
243    public
244    function optimize()
245    {
246
247        if ($this->getRequestedOptimizeOrDefault()) {
248
249            /**
250             * Delete Editor namespace
251             * https://github.com/svg/svgo/blob/master/plugins/removeEditorsNSData.js
252             */
253            $confNamespaceToKeeps = SiteConfig::getConfValue(FetcherSvg::CONF_OPTIMIZATION_NAMESPACES_TO_KEEP);
254            $namespaceToKeep = StringUtility::explodeAndTrim($confNamespaceToKeeps, ",");
255            foreach ($this->getXmlDocument()->getNamespaces() as $namespacePrefix => $namespaceUri) {
256                if (
257                    !empty($namespacePrefix)
258                    && $namespacePrefix != "svg"
259                    && !in_array($namespacePrefix, $namespaceToKeep)
260                    && in_array($namespaceUri, FetcherSvg::EDITOR_NAMESPACE)
261                ) {
262                    $this->getXmlDocument()->removeNamespace($namespaceUri);
263                }
264            }
265
266            /**
267             * Delete empty namespace rules
268             */
269            $documentElement = $this->getXmlDocument()->getDomDocument()->documentElement;
270            foreach ($this->getXmlDocument()->getNamespaces() as $namespacePrefix => $namespaceUri) {
271                $nodes = $this->getXmlDocument()->xpath("//*[namespace-uri()='$namespaceUri']");
272                $attributes = $this->getXmlDocument()->xpath("//@*[namespace-uri()='$namespaceUri']");
273                if ($nodes->length == 0 && $attributes->length == 0) {
274                    $result = $documentElement->removeAttributeNS($namespaceUri, $namespacePrefix);
275                    if ($result === false) {
276                        LogUtility::msg("Internal error: The deletion of the empty namespace ($namespacePrefix:$namespaceUri) didn't succeed", LogUtility::LVL_MSG_WARNING, "support");
277                    }
278                }
279            }
280
281            /**
282             * Delete comments
283             */
284            $commentNodes = $this->getXmlDocument()->xpath("//comment()");
285            foreach ($commentNodes as $commentNode) {
286                $this->getXmlDocument()->removeNode($commentNode);
287            }
288
289            /**
290             * Delete default value (version=1.1 for instance)
291             */
292            $defaultValues = FetcherSvg::SVG_DEFAULT_ATTRIBUTES_VALUE;
293            foreach ($documentElement->attributes as $attribute) {
294                /** @var DOMAttr $attribute */
295                $name = $attribute->name;
296                if (isset($defaultValues[$name])) {
297                    if ($defaultValues[$name] == $attribute->value) {
298                        $documentElement->removeAttributeNode($attribute);
299                    }
300                }
301            }
302
303            /**
304             * Suppress the attributes (by default id, style and class, data-name)
305             */
306            $attributeConfToDelete = SiteConfig::getConfValue(FetcherSvg::CONF_OPTIMIZATION_ATTRIBUTES_TO_DELETE, "id, style, class, data-name");
307            $attributesNameToDelete = StringUtility::explodeAndTrim($attributeConfToDelete, ",");
308            foreach ($attributesNameToDelete as $value) {
309
310                if (in_array($value, ["style", "class", "id"]) && $this->getRequestedPreserveStyleOrDefault()) {
311                    // we preserve the style, we preserve the class
312                    continue;
313                }
314
315                $nodes = $this->getXmlDocument()->xpath("//@$value");
316                foreach ($nodes as $node) {
317                    /** @var DOMAttr $node */
318                    /** @var DOMElement $DOMNode */
319                    $DOMNode = $node->parentNode;
320                    $DOMNode->removeAttributeNode($node);
321                }
322            }
323
324            /**
325             * Remove width/height that coincides with a viewBox attr
326             * https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute
327             * Example:
328             * <svg width="100" height="50" viewBox="0 0 100 50">
329             * <svg viewBox="0 0 100 50">
330             *
331             */
332            $widthAttributeValue = $documentElement->getAttribute("width");
333            if (!empty($widthAttributeValue)) {
334                $widthPixel = Unit::toPixel($widthAttributeValue);
335
336                $heightAttributeValue = $documentElement->getAttribute("height");
337                if (!empty($heightAttributeValue)) {
338                    $heightPixel = Unit::toPixel($heightAttributeValue);
339
340                    // ViewBox
341                    $viewBoxAttribute = $documentElement->getAttribute(FetcherSvg::VIEW_BOX);
342                    if (!empty($viewBoxAttribute)) {
343                        $viewBoxAttributeAsArray = StringUtility::explodeAndTrim($viewBoxAttribute, " ");
344
345                        if (sizeof($viewBoxAttributeAsArray) == 4) {
346                            $minX = $viewBoxAttributeAsArray[0];
347                            $minY = $viewBoxAttributeAsArray[1];
348                            $widthViewPort = $viewBoxAttributeAsArray[2];
349                            $heightViewPort = $viewBoxAttributeAsArray[3];
350                            if (
351                                $minX == 0 &
352                                $minY == 0 &
353                                $widthViewPort == $widthPixel &
354                                $heightViewPort == $heightPixel
355                            ) {
356                                $documentElement->removeAttribute("width");
357                                $documentElement->removeAttribute("height");
358                            }
359
360                        }
361                    }
362                }
363            }
364
365
366            /**
367             * Suppress script and style
368             *
369             *
370             * Delete of scripts https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
371             *
372             * And defs/style
373             *
374             * The style can leak in other icon/svg inlined in the document
375             *
376             * Technically on icon, there should be no `style`
377             * on inline icon otherwise, the css style can leak
378             *
379             * Example with carbon that use cls-1 on all icons
380             * https://github.com/carbon-design-system/carbon/issues/5568
381             * The facebook icon has a class cls-1 with an opacity of 0
382             * that leaks to the tumblr icon that has also a cls-1 class
383             *
384             * The illustration uses inline fill to color and styled
385             * For instance, all un-draw: https://undraw.co/illustrations
386             */
387            $elementsToDeleteConf = SiteConfig::getConfValue(FetcherSvg::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE, "script, style, title, desc");
388            $elementsToDelete = StringUtility::explodeAndTrim($elementsToDeleteConf, ",");
389            foreach ($elementsToDelete as $elementToDelete) {
390                if ($elementToDelete === "style" && $this->getRequestedPreserveStyleOrDefault()) {
391                    continue;
392                }
393                XmlSystems::deleteAllElementsByName($elementToDelete, $this->getXmlDocument());
394            }
395
396            // Delete If Empty
397            //   * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
398            //   * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata
399            $elementsToDeleteIfEmptyConf = SiteConfig::getConfValue(FetcherSvg::CONF_OPTIMIZATION_ELEMENTS_TO_DELETE_IF_EMPTY, "metadata, defs, g");
400            $elementsToDeleteIfEmpty = StringUtility::explodeAndTrim($elementsToDeleteIfEmptyConf);
401            foreach ($elementsToDeleteIfEmpty as $elementToDeleteIfEmpty) {
402                $elementNodeList = $this->getXmlDocument()->xpath("//*[local-name()='$elementToDeleteIfEmpty']");
403                foreach ($elementNodeList as $element) {
404                    /** @var DOMElement $element */
405                    if (!$element->hasChildNodes()) {
406                        $element->parentNode->removeChild($element);
407                    }
408                }
409            }
410
411            /**
412             * Delete the svg prefix namespace definition
413             * At the end to be able to query with svg as prefix
414             */
415            if (!in_array("svg", $namespaceToKeep)) {
416                $documentElement->removeAttributeNS(FetcherSvg::SVG_NAMESPACE_URI, FetcherSvg::SVG_NAMESPACE_PREFIX);
417            }
418
419        }
420    }
421
422
423    /**
424     * The height of the viewbox
425     * @return int
426     */
427    public function getIntrinsicHeight(): int
428    {
429
430        try {
431            $this->buildXmlDocumentIfNeeded();
432        } catch (ExceptionBadSyntax $e) {
433            throw new ExceptionBadSyntaxRuntime($e->getMessage(), self::CANONICAL, 1, $e);
434        }
435        return $this->intrinsicHeight;
436
437    }
438
439    /**
440     * The width of the view box
441     * @return int
442     */
443    public
444    function getIntrinsicWidth(): int
445    {
446        try {
447            $this->buildXmlDocumentIfNeeded();
448        } catch (ExceptionBadSyntax $e) {
449            throw new ExceptionBadSyntaxRuntime($e->getMessage(), self::CANONICAL, 1, $e);
450        }
451        return $this->intrinsicWidth;
452    }
453
454    /**
455     * @return string
456     * @throws ExceptionBadArgument
457     * @throws ExceptionBadState
458     * @throws ExceptionBadSyntax
459     * @throws ExceptionCompile
460     * @throws ExceptionNotFound
461     */
462    public function processAndGetMarkup(): string
463    {
464
465        return $this->process()->getMarkup();
466
467
468    }
469
470
471    /**
472     * @throws ExceptionBadState - if no svg was set to be processed
473     */
474    public function getMarkup(): string
475    {
476        return $this->getXmlDocument()->toXml();
477    }
478
479    /**
480     * @throws ExceptionBadSyntax
481     * @throws ExceptionNotFound
482     */
483    public function setSourcePath(WikiPath $path): IFetcherLocalImage
484    {
485
486        $this->setOriginalPathTraitAlias($path);
487        return $this;
488
489    }
490
491
492    /**
493     *
494     * @return Url - the fetch url
495     *
496     */
497    public function getFetchUrl(Url $url = null): Url
498    {
499
500        $url = parent::getFetchUrl($url);
501
502        /**
503         * Trait
504         */
505        $this->addLocalPathParametersToFetchUrl($url, MediaMarkup::$MEDIA_QUERY_PARAMETER);
506
507        /**
508         * Specific properties
509         */
510        try {
511            $url->addQueryParameter(ColorRgb::COLOR, $this->getRequestedColor()->toCssValue());
512        } catch (ExceptionNotFound $e) {
513            // no color ok
514        }
515        try {
516            $url->addQueryParameter(self::REQUESTED_PRESERVE_ASPECT_RATIO_KEY, $this->getRequestedPreserveAspectRatio());
517        } catch (ExceptionNotFound $e) {
518            // no preserve ratio ok
519        }
520        try {
521            $url->addQueryParameter(self::REQUESTED_NAME_ATTRIBUTE, $this->getRequestedName());
522        } catch (ExceptionNotFound $e) {
523            // no name
524        }
525        try {
526            $url->addQueryParameter(Dimension::ZOOM_ATTRIBUTE, $this->getRequestedZoom());
527        } catch (ExceptionNotFound $e) {
528            // no name
529        }
530        try {
531            $url->addQueryParameter(TagAttributes::CLASS_KEY, $this->getRequestedClass());
532        } catch (ExceptionNotFound $e) {
533            // no name
534        }
535        try {
536            $url->addQueryParameter(TagAttributes::TYPE_KEY, $this->getRequestedType());
537        } catch (ExceptionNotFound $e) {
538            // no name
539        }
540
541        return $url;
542
543    }
544
545    /**
546     * @throws ExceptionNotFound
547     */
548    public function getRequestedPreserveAspectRatio(): string
549    {
550        if ($this->preserveAspectRatio === null) {
551            throw new ExceptionNotFound("No preserve Aspect Ratio was requested");
552        }
553        return $this->preserveAspectRatio;
554    }
555
556    /**
557     * Return the svg file transformed by the attributes
558     * from cache if possible. Used when making a fetch with the URL
559     * @return LocalPath
560     * @throws ExceptionBadArgument
561     * @throws ExceptionBadState
562     * @throws ExceptionBadSyntax - the file is not a svg file
563     * @throws ExceptionCompile
564     * @throws ExceptionNotFound - the file was not found
565     */
566    public function getFetchPath(): LocalPath
567    {
568
569        /**
570         * Generated svg file cache init
571         */
572        $fetchCache = FetcherCache::createFrom($this);
573        $files[] = $this->getSourcePath();
574        try {
575            $files[] = ClassUtility::getClassPath(FetcherSvg::class);
576        } catch (\ReflectionException $e) {
577            LogUtility::internalError("Unable to add the FetchImageSvg class as dependency. Error: {$e->getMessage()}");
578        }
579        try {
580            $files[] = ClassUtility::getClassPath(XmlDocument::class);
581        } catch (\ReflectionException $e) {
582            LogUtility::internalError("Unable to add the XmlDocument class as dependency. Error: {$e->getMessage()}");
583        }
584        $files = array_merge(Site::getConfigurationFiles(), $files); // svg generation depends on configuration
585        foreach ($files as $file) {
586            $fetchCache->addFileDependency($file);
587        }
588
589        global $ACT;
590        if (PluginUtility::isDev() && $ACT === ExecutionContext::PREVIEW_ACTION) {
591            // in dev mode, don't cache
592            $isCacheUsable = false;
593        } else {
594            $isCacheUsable = $fetchCache->isCacheUsable();
595        }
596        if (!$isCacheUsable) {
597            $content = self::processAndGetMarkup();
598            $fetchCache->storeCache($content);
599        }
600        return $fetchCache->getFile();
601
602    }
603
604    /**
605     * The buster is also based on the configuration file
606     *
607     * It the user changes the configuration, the svg file is generated
608     * again and the browser cache should be deleted (ie the buster regenerated)
609     *
610     * {@link ResourceCombo::getBuster()}
611     * @return string
612     *
613     * @throws ExceptionNotFound
614     */
615    public function getBuster(): string
616    {
617        $buster = FileSystems::getCacheBuster($this->getSourcePath());
618        try {
619            $configFile = FileSystems::getCacheBuster(DirectoryLayout::getConfLocalFilePath());
620            $buster = "$buster-$configFile";
621        } catch (ExceptionNotFound $e) {
622            // no local conf file
623            if (PluginUtility::isDevOrTest()) {
624                LogUtility::internalError("A local configuration file should be present in dev");
625            }
626        }
627        return $buster;
628
629    }
630
631
632    function acceptsFetchUrl(Url $url): bool
633    {
634
635        try {
636            $dokuPath = FetcherRawLocalPath::createEmpty()->buildFromUrl($url)->processIfNeededAndGetFetchPath();
637        } catch (ExceptionBadArgument $e) {
638            return false;
639        }
640        try {
641            $mime = FileSystems::getMime($dokuPath);
642        } catch (ExceptionNotFound $e) {
643            return false;
644        }
645        if ($mime->toString() === Mime::SVG) {
646            return true;
647        }
648        return false;
649
650    }
651
652    public function getMime(): Mime
653    {
654        return Mime::create(Mime::SVG);
655    }
656
657
658    public function setRequestedColor(ColorRgb $color): FetcherSvg
659    {
660        $this->color = $color;
661        return $this;
662    }
663
664    /**
665     * @throws ExceptionNotFound
666     */
667    public function getRequestedColor(): ColorRgb
668    {
669        if ($this->color === null) {
670            throw new ExceptionNotFound("No requested color");
671        }
672        return $this->color;
673    }
674
675    /**
676     * @param string $preserveAspectRatio - the aspect ratio of the svg
677     * @return $this
678     */
679    public function setRequestedPreserveAspectRatio(string $preserveAspectRatio): FetcherSvg
680    {
681        $this->preserveAspectRatio = $preserveAspectRatio;
682        return $this;
683    }
684
685
686    /**
687     * @var string|null - a name identifier that is added in the SVG
688     */
689    private ?string $requestedName = null;
690
691    /**
692     * @var ?boolean do the svg should be optimized
693     */
694    private ?bool $requestedOptimization = null;
695
696    /**
697     * @var XmlDocument|null
698     */
699    private ?XmlDocument $xmlDocument = null;
700
701
702    /**
703     * The name:
704     *  * if this is a icon, this is the icon name of the {@link IconDownloader}. It's used to download the icon if not present.
705     *  * is used to add a data attribute in the svg to be able to select it for test purpose
706     *
707     * @param string $name
708     * @return FetcherSvg
709     */
710    public
711    function setRequestedName(string $name): FetcherSvg
712    {
713        $this->requestedName = $name;
714        return $this;
715    }
716
717
718    public
719    function __toString()
720    {
721        if (isset($this->name)) {
722            return $this->name;
723        }
724        if (isset($this->path)) {
725            try {
726                return $this->path->getLastNameWithoutExtension();
727            } catch (ExceptionNotFound $e) {
728                LogUtility::internalError("root not possible, we should have a last name", self::CANONICAL);
729                return "Anonymous";
730            }
731        }
732        return "Anonymous";
733
734
735    }
736
737
738    /**
739     * @param string $viewBox
740     * @return string[]
741     */
742    private function getViewBoxAttributes(string $viewBox): array
743    {
744        $attributes = explode(" ", $viewBox);
745        if (sizeof($attributes) === 1) {
746            /**
747             * We may find also comma. Example:
748             * viewBox="0,0,433.62,289.08"
749             */
750            $attributes = explode(",", $viewBox);
751        }
752        return $attributes;
753    }
754
755
756    private function getXmlDocument(): XmlDocument
757    {
758
759        $this->buildXmlDocumentIfNeeded();
760        return $this->xmlDocument;
761    }
762
763    /**
764     * Utility function
765     * @return \DOMDocument
766     */
767    public function getXmlDom(): \DOMDocument
768    {
769        return $this->getXmlDocument()->getDomDocument();
770    }
771
772    /**
773     * @throws ExceptionNotFound
774     */
775    public function getRequestedName(): string
776    {
777        if ($this->requestedName === null) {
778            throw new ExceptionNotFound("Name was not set");
779        }
780        return $this->requestedName;
781    }
782
783    public function setPreserveStyle(bool $bool): FetcherSvg
784    {
785        $this->preserveStyle = $bool;
786        return $this;
787    }
788
789    public function getRequestedPreserveStyleOrDefault(): bool
790    {
791        try {
792            return $this->getRequestedPreserveStyle();
793        } catch (ExceptionNotFound $e) {
794            return false;
795        }
796    }
797
798    /**
799     * @throws ExceptionNotFound
800     */
801    public function getRequestedType(): string
802    {
803        if ($this->requestedType === null) {
804            throw new ExceptionNotFound("The requested type was not specified");
805        }
806        return $this->requestedType;
807    }
808
809    /**
810     * @param string $markup - the svg as a string
811     * @param string $name - a name identifier (used in diff)
812     * @throws ExceptionBadSyntax
813     */
814    private function setMarkup(string $markup, string $name): FetcherSvg
815    {
816        $this->name = $name;
817        $this->buildXmlDocumentIfNeeded($markup);
818        return $this;
819    }
820
821
822    public function setRequestedType(string $requestedType): FetcherSvg
823    {
824        $this->requestedType = $requestedType;
825        return $this;
826    }
827
828    public function getRequestedHeight(): int
829    {
830        try {
831            return $this->getDefaultWidhtAndHeightForIconAndTileIfNotSet();
832        } catch (ExceptionNotFound $e) {
833            return parent::getRequestedHeight();
834        }
835    }
836
837    public function getRequestedWidth(): int
838    {
839        try {
840            return $this->getDefaultWidhtAndHeightForIconAndTileIfNotSet();
841        } catch (ExceptionNotFound $e) {
842            return parent::getRequestedWidth();
843        }
844    }
845
846    /**
847     * @throws ExceptionBadSyntax
848     * @throws ExceptionBadArgument
849     * @throws ExceptionBadState
850     * @throws ExceptionCompile
851     */
852    public function process(): FetcherSvg
853    {
854
855        if ($this->processed) {
856            LogUtility::internalError("The svg was already processed");
857            return $this;
858        }
859
860        $this->processed = true;
861
862        // Handy variable
863        $documentElement = $this->getXmlDocument()->getElement();
864
865
866        if ($this->getRequestedOptimizeOrDefault()) {
867            $this->optimize();
868        }
869
870        // Set the name (icon) attribute for test selection
871        try {
872            $name = $this->getRequestedNameOrDefault();
873            $documentElement->setAttribute('data-name', $name);
874        } catch (ExceptionNotFound $e) {
875            // ok no name
876        }
877
878
879        // Width requested
880        try {
881            $requestedWidth = $this->getRequestedWidth();
882        } catch (ExceptionNotFound $e) {
883            $requestedWidth = null;
884        }
885
886        // Height requested
887        try {
888            $requestedHeight = $this->getRequestedHeight();
889        } catch (ExceptionNotFound $e) {
890            $requestedHeight = null;
891        }
892
893
894        try {
895            $requestedType = $this->getRequestedType();
896        } catch (ExceptionNotFound $e) {
897            $requestedType = null;
898        }
899
900        /**
901         * Svg Structure
902         *
903         * All attributes that are applied for all usage (output independent)
904         * and that depends only on the structure of the icon
905         *
906         * Why ? Because {@link \syntax_plugin_combo_pageimage}
907         * can be an icon or an illustrative image
908         *
909         */
910        $intrinsicWidth = $this->getIntrinsicWidth();
911        $intrinsicHeight = $this->getIntrinsicHeight();
912
913
914        $svgStructureType = $this->getInternalStructureType();
915
916
917        /**
918         * Svg type
919         * The svg type is the svg usage
920         * How the svg should be shown (the usage)
921         *
922         * We need it to make the difference between an icon
923         *   * in a paragraph (the width and height are the same)
924         *   * as an illustration in a page image (the width and height may be not the same)
925         */
926        if ($requestedType === null) {
927            switch ($svgStructureType) {
928                case FetcherSvg::ICON_TYPE:
929                    $requestedType = FetcherSvg::ICON_TYPE;
930                    break;
931                default:
932                    $requestedType = FetcherSvg::ILLUSTRATION_TYPE;
933                    break;
934            }
935        }
936
937        /**
938         * A tag attributes to manage the add of style properties
939         * in the style attribute
940         */
941        $extraAttributes = TagAttributes::createEmpty(self::TAG);
942
943        /**
944         * Zoom occurs after the crop/dimenions setting if any
945         */
946        try {
947            $zoomFactor = $this->getRequestedZoom();
948        } catch (ExceptionNotFound $e) {
949            if ($svgStructureType === FetcherSvg::ICON_TYPE && $requestedType === FetcherSvg::ILLUSTRATION_TYPE) {
950                $zoomFactor = -4;
951            } else {
952                $zoomFactor = 1; // 0r 1 :)
953            }
954        }
955
956
957        /**
958         * Dimension processing (heigth, width, viewbox)
959         *
960         * ViewBox should exist
961         *
962         * Ratio / Width / Height Cropping happens via the viewbox
963         *
964         * Width and height used to set the viewBox of a svg
965         * to crop it (In a raster image, there is not this distinction)
966         *
967         * We set the viewbox everytime:
968         * If width and height are not the same, this is a crop
969         * If width and height are the same, this is not a crop
970         */
971        $targetWidth = $this->getTargetWidth();
972        $targetHeight = $this->getTargetHeight();
973        if ($this->isCropRequested() || $zoomFactor !== 1) {
974
975            /**
976             * ViewBox is the logical view
977             *
978             * with an icon case, we zoom out for illustation otherwise, this is ugly as the icon takes the whole place
979             *
980             * Zoom applies on the target/cropped dimension
981             * so that we can center all at once in the next step
982             */
983
984            /**
985             * The crop happens when we set the height and width on the svg.
986             * There is no need to manipulate the view box coordinate
987             */
988
989            /**
990             * Note: if the svg is an icon of width 24 with a viewbox of 0 0 24 24,
991             * if you double the viewbox to 0 0 48 48, you have applied of -2
992             * The icon is two times smaller smaller
993             */
994            $viewBoxWidth = $this->getIntrinsicWidth();
995            $viewBoxHeight = $this->getIntrinsicHeight();
996            if ($zoomFactor < 0) {
997                $viewBoxWidth = -$zoomFactor * $viewBoxWidth;
998                $viewBoxHeight = -$zoomFactor * $viewBoxHeight;
999            } else {
1000                $viewBoxWidth = $viewBoxWidth / $zoomFactor;
1001                $viewBoxHeight = $viewBoxHeight / $zoomFactor;
1002            }
1003
1004
1005            /**
1006             * Center
1007             *
1008             * We center by moving the origin (ie x and y)
1009             */
1010            $x = -($viewBoxWidth - $intrinsicWidth) / 2;
1011            $y = -($viewBoxHeight - $intrinsicHeight) / 2;
1012            $documentElement->setAttribute(FetcherSvg::VIEW_BOX, "$x $y $viewBoxWidth $viewBoxHeight");
1013
1014        } else {
1015            $viewBox = $documentElement->getAttribute(FetcherSvg::VIEW_BOX);
1016            if (empty($viewBox)) {
1017                // viewbox is mandatory
1018                $documentElement->setAttribute(FetcherSvg::VIEW_BOX, "0 0 {$this->getIntrinsicWidth()} {$this->getIntrinsicHeight()}");
1019            }
1020        }
1021        /**
1022         * Dimension are mandatory
1023         * Why ?
1024         * - to not take the dimension of the parent - Setting the width and height is important, otherwise it takes the dimension of the parent (that are generally a squared)
1025         * - to show the crop
1026         * - to have internal calculate dimension otherwise, it's tiny
1027         * - To have an internal width and not shrink on the css property `width: auto !important;` of a table
1028         * - To have an internal height and not shrink on the css property `height: auto !important;` of a table
1029         * - Using a icon in the navbrand component of bootstrap require the set of width and height otherwise the svg has a calculated width of null and the bar component are below the brand text
1030         * - ...
1031         * */
1032        $documentElement
1033            ->setAttribute(Dimension::WIDTH_KEY, $targetWidth)
1034            ->setAttribute(Dimension::HEIGHT_KEY, $targetHeight);
1035
1036        /**
1037         * Css styling due to dimension
1038         */
1039        switch ($requestedType) {
1040            case FetcherSvg::ICON_TYPE:
1041            case FetcherSvg::TILE_TYPE:
1042
1043                if ($targetWidth !== $targetHeight) {
1044                    /**
1045                     * Check if the widht and height are the same
1046                     *
1047                     * Note: this is not the case for an illustration,
1048                     * they may be different
1049                     * They are not the width and height of the icon but
1050                     * the width and height of the viewbox
1051                     */
1052                    LogUtility::info("An icon or tile is defined as having the same dimension but the svg ($this) has a target width of ($targetWidth) that is different from the target height ($targetHeight). The icon will be cropped.");
1053                }
1054
1055                break;
1056            default:
1057                /**
1058                 * Illustration / Image
1059                 */
1060                /**
1061                 * Responsive SVG
1062                 */
1063                try {
1064                    $aspectRatio = $this->getRequestedPreserveAspectRatio();
1065                } catch (ExceptionNotFound $e) {
1066                    /**
1067                     *
1068                     * Keep the same height
1069                     * Image in the Middle and border deleted when resizing
1070                     * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
1071                     * Default is xMidYMid meet
1072                     */
1073                    $aspectRatio = SiteConfig::getConfValue(FetcherSvg::CONF_PRESERVE_ASPECT_RATIO_DEFAULT, "xMidYMid slice");
1074                }
1075                $documentElement->setAttribute("preserveAspectRatio", $aspectRatio);
1076
1077                /**
1078                 * Note on dimension width and height
1079                 * Width and height element attribute are in reality css style properties.
1080                 *   ie the max-width style
1081                 * They are treated in {@link PluginUtility::processStyle()}
1082                 */
1083
1084                /**
1085                 * Adapt to the container by default
1086                 * Height `auto` and not `100%` otherwise you get a layout shift
1087                 */
1088                $extraAttributes->addStyleDeclarationIfNotSet("width", "100%");
1089                $extraAttributes->addStyleDeclarationIfNotSet("height", "auto");
1090
1091
1092                if ($requestedWidth !== null) {
1093
1094                    /**
1095                     * If a dimension was set, it's seen by default as a max-width
1096                     * If it should not such as in a card, this property is already set
1097                     * and is not overwritten
1098                     */
1099                    try {
1100                        $widthInPixel = ConditionalLength::createFromString($requestedWidth)->toPixelNumber();
1101                    } catch (ExceptionCompile $e) {
1102                        LogUtility::msg("The requested width $requestedWidth could not be converted to pixel. It returns the following error ({$e->getMessage()}). Processing was stopped");
1103                        return $this;
1104                    }
1105                    $extraAttributes->addStyleDeclarationIfNotSet("max-width", "{$widthInPixel}px");
1106
1107                }
1108
1109
1110                if ($requestedHeight !== null) {
1111                    /**
1112                     * If a dimension was set, it's seen by default as a max-width
1113                     * If it should not such as in a card, this property is already set
1114                     * and is not overwritten
1115                     */
1116                    try {
1117                        $heightInPixel = ConditionalLength::createFromString($requestedHeight)->toPixelNumber();
1118                    } catch (ExceptionCompile $e) {
1119                        LogUtility::msg("The requested height $requestedHeight could not be converted to pixel. It returns the following error ({$e->getMessage()}). Processing was stopped");
1120                        return $this;
1121                    }
1122                    $extraAttributes->addStyleDeclarationIfNotSet("max-height", "{$heightInPixel}px");
1123
1124
1125                }
1126
1127                break;
1128        }
1129
1130
1131        switch ($svgStructureType) {
1132            case FetcherSvg::ICON_TYPE:
1133            case FetcherSvg::TILE_TYPE:
1134                /**
1135                 * Determine if this is a:
1136                 *   * fill one color
1137                 *   * fill two colors
1138                 *   * or stroke svg icon
1139                 *
1140                 * The color can be set:
1141                 *   * on fill (surface)
1142                 *   * on stroke (line)
1143                 *
1144                 * If the stroke attribute is not present this is a fill icon
1145                 */
1146                $svgColorType = FetcherSvg::COLOR_TYPE_FILL_SOLID;
1147                if ($documentElement->hasAttribute(FetcherSvg::STROKE_ATTRIBUTE)) {
1148                    $svgColorType = FetcherSvg::COLOR_TYPE_STROKE_OUTLINE;
1149                }
1150                /**
1151                 * Double color icon ?
1152                 */
1153                $isDoubleColor = false;
1154                if ($svgColorType === FetcherSvg::COLOR_TYPE_FILL_SOLID) {
1155                    $svgFillsElement = $this->getXmlDocument()->xpath("//*[@fill]");
1156                    $fillColors = [];
1157                    for ($i = 0; $i < $svgFillsElement->length; $i++) {
1158                        /**
1159                         * @var DOMElement $nodeElement
1160                         */
1161                        $nodeElement = $svgFillsElement[$i];
1162                        $value = $nodeElement->getAttribute("fill");
1163                        if ($value !== "none") {
1164                            /**
1165                             * Icon may have none alongside colors
1166                             * Example:
1167                             */
1168                            $fillColors[$value] = $value;
1169                        }
1170                    }
1171                    if (sizeof($fillColors) > 1) {
1172                        $isDoubleColor = true;
1173                    }
1174                }
1175
1176                /**
1177                 * CurrentColor
1178                 *
1179                 * By default, the icon should have this property when downloaded
1180                 * but if this not the case (such as for Material design), we set them
1181                 *
1182                 * Feather set it on the stroke
1183                 * Example: view-source:https://raw.githubusercontent.com/feathericons/feather/master/icons/airplay.svg
1184                 * <svg
1185                 *  fill="none"
1186                 *  stroke="currentColor">
1187                 */
1188                if (!$isDoubleColor && !$documentElement->hasAttribute("fill")) {
1189
1190                    /**
1191                     * Note: if fill was not set, the default color would be black
1192                     */
1193                    $documentElement->setAttribute("fill", FetcherSvg::CURRENT_COLOR);
1194
1195                }
1196
1197
1198                /**
1199                 * Eva/Carbon Source Icon are not optimized at the source
1200                 * Example:
1201                 *   * eva:facebook-fill
1202                 *   * carbon:logo-tumblr (https://github.com/carbon-design-system/carbon/issues/5568)
1203                 *
1204                 * We delete the rectangle
1205                 * Style should have already been deleted by the optimization
1206                 *
1207                 * This optimization should happen if the color is set
1208                 * or not because we set the color value to `currentColor`
1209                 *
1210                 * If the rectangle stay, we just see a black rectangle
1211                 */
1212                try {
1213                    $path = $this->getSourcePath();
1214                    $pathString = $path->toAbsolutePath()->toAbsoluteId();
1215                    if (
1216                        preg_match("/carbon|eva/i", $pathString) === 1
1217                    ) {
1218                        XmlSystems::deleteAllElementsByName("rect", $this->getXmlDocument());
1219                    }
1220                } catch (ExceptionNotFound $e) {
1221                    // ok
1222                }
1223
1224
1225                $color = null;
1226                try {
1227                    $color = $this->getRequestedColor();
1228                } catch (ExceptionNotFound $e) {
1229                    if ($requestedType === FetcherSvg::ILLUSTRATION_TYPE) {
1230                        $primaryColor = Site::getPrimaryColorValue();
1231                        if ($primaryColor !== null) {
1232                            $color = ColorRgb::createFromString($primaryColor);
1233                        }
1234                    }
1235                }
1236
1237
1238                /**
1239                 * Color
1240                 * Color applies only if this is an icon.
1241                 *
1242                 */
1243                if ($color !== null) {
1244                    /**
1245                     *
1246                     * We say that this is used only for an icon (<72 px)
1247                     *
1248                     * Not that an icon svg file can also be used as {@link \syntax_plugin_combo_pageimage}
1249                     *
1250                     * We don't set it as a styling attribute
1251                     * because it's not taken into account if the
1252                     * svg is used as a background image
1253                     * fill or stroke should have at minimum "currentColor"
1254                     */
1255                    $colorValue = $color->toCssValue();
1256
1257
1258                    switch ($svgColorType) {
1259                        case FetcherSvg::COLOR_TYPE_FILL_SOLID:
1260
1261                            if (!$isDoubleColor) {
1262
1263                                $documentElement->setAttribute("fill", $colorValue);
1264
1265                                if ($colorValue !== FetcherSvg::CURRENT_COLOR) {
1266                                    /**
1267                                     * Update the fill property on sub-path
1268                                     * If the fill is set on sub-path, it will not work
1269                                     *
1270                                     * fill may be set on group or whatever
1271                                     */
1272                                    $svgPaths = $this->getXmlDocument()->xpath("//*[local-name()='path' or local-name()='g']");
1273                                    for ($i = 0; $i < $svgPaths->length; $i++) {
1274                                        /**
1275                                         * @var DOMElement $nodeElement
1276                                         */
1277                                        $nodeElement = $svgPaths[$i];
1278                                        $value = $nodeElement->getAttribute("fill");
1279                                        if ($value !== "none") {
1280                                            if ($nodeElement->parentNode->tagName !== "svg") {
1281                                                $nodeElement->setAttribute("fill", FetcherSvg::CURRENT_COLOR);
1282                                            } else {
1283                                                $this->getXmlDocument()->removeAttributeValue("fill", $nodeElement);
1284                                            }
1285                                        }
1286                                    }
1287
1288                                }
1289                            } else {
1290                                // double color
1291                                $firsFillElement = $this->getXmlDocument()->xpath("//*[@fill][1]")->item(0);
1292                                if ($firsFillElement instanceof DOMElement) {
1293                                    $firsFillElement->setAttribute("fill", $colorValue);
1294                                }
1295                            }
1296                            break;
1297
1298                        case FetcherSvg::COLOR_TYPE_STROKE_OUTLINE:
1299                            $documentElement->setAttribute("fill", "none");
1300                            $documentElement->setAttribute(FetcherSvg::STROKE_ATTRIBUTE, $colorValue);
1301
1302                            if ($colorValue !== FetcherSvg::CURRENT_COLOR) {
1303                                /**
1304                                 * Delete the stroke property on sub-path
1305                                 */
1306                                // if the fill is set on sub-path, it will not work
1307                                $svgPaths = $this->getXmlDocument()->xpath("//*[local-name()='path']");
1308                                for ($i = 0; $i < $svgPaths->length; $i++) {
1309                                    /**
1310                                     * @var DOMElement $nodeElement
1311                                     */
1312                                    $nodeElement = $svgPaths[$i];
1313                                    $value = $nodeElement->getAttribute(FetcherSvg::STROKE_ATTRIBUTE);
1314                                    if ($value !== "none") {
1315                                        $this->getXmlDocument()->removeAttributeValue(FetcherSvg::STROKE_ATTRIBUTE, $nodeElement);
1316                                    } else {
1317                                        $this->getXmlDocument()->removeNode($nodeElement);
1318                                    }
1319                                }
1320
1321                            }
1322                            break;
1323                    }
1324
1325                }
1326                break;
1327
1328        }
1329
1330
1331        /**
1332         * Set the attributes to the root element
1333         * Svg attribute are case sensitive
1334         * Styling
1335         */
1336        $extraAttributeAsArray = $extraAttributes->toHtmlArray();
1337        foreach ($extraAttributeAsArray as $name => $value) {
1338            $documentElement->setAttribute($name, $value);
1339        }
1340
1341        /**
1342         * Class
1343         */
1344        try {
1345            $class = $this->getRequestedClass();
1346            $documentElement->addClass($class);
1347        } catch (ExceptionNotFound $e) {
1348            // no class
1349        }
1350        // add class with svg type
1351        $documentElement
1352            ->addClass(StyleAttribute::addComboStrapSuffix(self::TAG))
1353            ->addClass(StyleAttribute::addComboStrapSuffix(self::TAG . "-" . $requestedType));
1354        // Add a class on each path for easy styling
1355        try {
1356            $name = $this->getRequestedNameOrDefault();
1357            $svgPaths = $documentElement->querySelectorAll('path');
1358            for ($i = 0;
1359                 $i < count($svgPaths);
1360                 $i++) {
1361                $element = $svgPaths[$i];
1362                $stylingClass = $name . "-" . $i;
1363                $element->addClass($stylingClass);
1364            }
1365        } catch (ExceptionNotFound $e) {
1366            // no name
1367        }
1368
1369        return $this;
1370
1371    }
1372
1373
1374    public function getFetcherName(): string
1375    {
1376        return self::CANONICAL;
1377    }
1378
1379    /**
1380     * @throws ExceptionBadArgument
1381     * @throws ExceptionBadSyntax
1382     * @throws ExceptionCompile
1383     */
1384    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherImage
1385    {
1386
1387        foreach (array_keys($tagAttributes->getComponentAttributes()) as $svgAttribute) {
1388            $svgAttribute = strtolower($svgAttribute);
1389            switch ($svgAttribute) {
1390                case Dimension::WIDTH_KEY:
1391                case Dimension::HEIGHT_KEY:
1392                    /**
1393                     * Length may be defined with CSS unit
1394                     * https://www.w3.org/TR/SVG2/coords.html#Units
1395                     */
1396                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1397                    try {
1398                        $lengthInt = ConditionalLength::createFromString($value)->toPixelNumber();
1399                    } catch (ExceptionBadArgument $e) {
1400                        LogUtility::error("The $svgAttribute value ($value) of the svg ($this) is not an integer", self::CANONICAL);
1401                        continue 2;
1402                    }
1403                    if ($svgAttribute === Dimension::WIDTH_KEY) {
1404                        $this->setRequestedWidth($lengthInt);
1405                    } else {
1406                        $this->setRequestedHeight($lengthInt);
1407                    }
1408                    continue 2;
1409                case Dimension::ZOOM_ATTRIBUTE;
1410                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1411                    try {
1412                        $lengthFloat = DataType::toFloat($value);
1413                    } catch (ExceptionBadArgument $e) {
1414                        LogUtility::error("The $svgAttribute value ($value) of the svg ($this) is not a float", self::CANONICAL);
1415                        continue 2;
1416                    }
1417                    $this->setRequestedZoom($lengthFloat);
1418                    continue 2;
1419                case ColorRgb::COLOR:
1420                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1421                    try {
1422                        $color = ColorRgb::createFromString($value);
1423                    } catch (ExceptionBadArgument $e) {
1424                        LogUtility::error("The $svgAttribute value ($value) of the svg ($this) is not an valid color", self::CANONICAL);
1425                        continue 2;
1426                    }
1427                    $this->setRequestedColor($color);
1428                    continue 2;
1429                case TagAttributes::TYPE_KEY:
1430                    $value = $tagAttributes->getValue($svgAttribute);
1431                    $this->setRequestedType($value);
1432                    continue 2;
1433                case self::REQUESTED_PRESERVE_ATTRIBUTE:
1434                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1435                    if ($value === "style") {
1436                        $preserve = true;
1437                    } else {
1438                        $preserve = false;
1439                    }
1440                    $this->setPreserveStyle($preserve);
1441                    continue 2;
1442                case self::NAME_ATTRIBUTE:
1443                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1444                    $this->setRequestedName($value);
1445                    continue 2;
1446                case TagAttributes::CLASS_KEY:
1447                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1448                    $this->setRequestedClass($value);
1449                    continue 2;
1450                case strtolower(self::REQUESTED_PRESERVE_ASPECT_RATIO_KEY):
1451                    $value = $tagAttributes->getValueAndRemove($svgAttribute);
1452                    $this->setRequestedPreserveAspectRatio($value);
1453                    continue 2;
1454            }
1455
1456        }
1457
1458        /**
1459         * Icon case
1460         */
1461        try {
1462            $iconDownload =
1463                !$tagAttributes->hasAttribute(MediaMarkup::$MEDIA_QUERY_PARAMETER) &&
1464                $this->getRequestedType() === self::ICON_TYPE
1465                && $this->getRequestedName() !== null;
1466            if ($iconDownload) {
1467                try {
1468                    $dokuPath = $this->downloadAndGetIconPath();
1469                } catch (ExceptionCompile $e) {
1470                    throw new ExceptionBadArgument("We can't get the icon path. Error: {$e->getMessage()}. (ie media or icon name attribute is mandatory).", self::CANONICAL, 1, $e);
1471                }
1472                $this->setSourcePath($dokuPath);
1473
1474            }
1475        } catch (ExceptionNotFound $e) {
1476            // no requested type or name
1477        }
1478
1479        /**
1480         * Raw Trait
1481         */
1482        $this->buildOriginalPathFromTagAttributes($tagAttributes);
1483        parent::buildFromTagAttributes($tagAttributes);
1484        return $this;
1485    }
1486
1487    /**
1488     * @throws ExceptionBadArgument
1489     * @throws ExceptionCompile
1490     * @throws ExceptionBadSyntax
1491     * @throws ExceptionNotFound
1492     */
1493    private function downloadAndGetIconPath(): WikiPath
1494    {
1495        /**
1496         * It may be a Svg icon that we needs to download
1497         */
1498        try {
1499            $requestedType = $this->getRequestedType();
1500            $requestedName = $this->getRequestedName();
1501        } catch (ExceptionNotFound $e) {
1502            throw new ExceptionNotFound("No path was defined and no icon name was defined");
1503        }
1504        if ($requestedType !== self::ICON_TYPE) {
1505            throw new ExceptionNotFound("No original path was set and no icon was defined");
1506        }
1507
1508        try {
1509            $iconDownloader = IconDownloader::createFromName($requestedName);
1510        } catch (ExceptionBadArgument $e) {
1511            throw new ExceptionNotFound("The name ($requestedName) is not a valid icon name. Error: ({$e->getMessage()}.", self::CANONICAL, 1, $e);
1512        }
1513        $originalPath = $iconDownloader->getPath();
1514        if (FileSystems::exists($originalPath)) {
1515            return $originalPath;
1516        }
1517        try {
1518            $iconDownloader->download();
1519        } catch (ExceptionCompile $e) {
1520            throw new ExceptionCompile("The icon ($requestedName) could not be downloaded. Error: ({$e->getMessage()}.", self::CANONICAL);
1521        }
1522        $this->setSourcePath($originalPath);
1523        return $originalPath;
1524    }
1525
1526    /**
1527     * This is used to add a name and class to the svg to make selection more easy
1528     * @throws ExceptionBadState
1529     * @throws ExceptionNotFound
1530     */
1531    private function getRequestedNameOrDefault(): string
1532    {
1533        try {
1534            return $this->getRequestedName();
1535        } catch (ExceptionNotFound $e) {
1536            return $this->getSourcePath()->getLastNameWithoutExtension();
1537        }
1538    }
1539
1540    /**
1541     * @return bool - true if no width or height was requested
1542     */
1543    private function norWidthNorHeightWasRequested(): bool
1544    {
1545
1546        if ($this->requestedWidth !== null) {
1547            return false;
1548        }
1549        if ($this->requestedHeight !== null) {
1550            return false;
1551        }
1552        return true;
1553
1554    }
1555
1556    /**
1557     * @throws ExceptionNotFound
1558     */
1559    private function getRequestedZoom(): float
1560    {
1561        $zoom = $this->zoomFactor;
1562        if ($zoom === null) {
1563            throw new ExceptionNotFound("No zoom requested");
1564        }
1565        return $zoom;
1566    }
1567
1568    public function setRequestedZoom(float $zoomFactor): FetcherSvg
1569    {
1570        $this->zoomFactor = $zoomFactor;
1571        return $this;
1572    }
1573
1574    public function setRequestedClass(string $value): FetcherSvg
1575    {
1576        $this->requestedClass = $value;
1577        return $this;
1578
1579    }
1580
1581    /**
1582     * @throws ExceptionNotFound
1583     */
1584    private function getRequestedClass(): string
1585    {
1586        if ($this->requestedClass === null) {
1587            throw new ExceptionNotFound("No class was set");
1588        }
1589        return $this->requestedClass;
1590    }
1591
1592    /**
1593     * Analyse and set the mandatory intrinsic dimensions
1594     * @throws ExceptionBadSyntax
1595     */
1596    private function setIntrinsicDimensions()
1597    {
1598        $this->setIntrinsicHeight()
1599            ->setIntrinsicWidth();
1600    }
1601
1602    /**
1603     * @throws ExceptionBadSyntax
1604     */
1605    private function setIntrinsicHeight(): FetcherSvg
1606    {
1607        $viewBox = $this->getXmlDocument()->getDomDocument()->documentElement->getAttribute(FetcherSvg::VIEW_BOX);
1608        if ($viewBox !== "") {
1609            $attributes = $this->getViewBoxAttributes($viewBox);
1610            $viewBoxHeight = $attributes[3];
1611            try {
1612                /**
1613                 * Ceil because we want to see a border if there is one
1614                 */
1615                $this->intrinsicHeight = DataType::toIntegerCeil($viewBoxHeight);
1616                return $this;
1617            } catch (ExceptionBadArgument $e) {
1618                throw new ExceptionBadSyntax("The media height ($viewBoxHeight) of the svg image ($this) is not a valid integer value");
1619            }
1620        }
1621        /**
1622         * Case with some icon such as
1623         * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg
1624         */
1625        $height = $this->getXmlDocument()->getDomDocument()->documentElement->getAttribute("height");
1626        if ($height === "") {
1627            throw new ExceptionBadSyntax("The svg ($this) does not have a viewBox or height attribute, the intrinsic height cannot be determined");
1628        }
1629        try {
1630            $this->intrinsicHeight = DataType::toInteger($height);
1631        } catch (ExceptionBadArgument $e) {
1632            throw new ExceptionBadSyntax("The media width ($height) of the svg image ($this) is not a valid integer value");
1633        }
1634        return $this;
1635    }
1636
1637    /**
1638     * @throws ExceptionBadSyntax
1639     */
1640    private function setIntrinsicWidth(): FetcherSvg
1641    {
1642        $viewBox = $this->getXmlDom()->documentElement->getAttribute(FetcherSvg::VIEW_BOX);
1643        if ($viewBox !== "") {
1644            $attributes = $this->getViewBoxAttributes($viewBox);
1645            $viewBoxWidth = $attributes[2];
1646            try {
1647                /**
1648                 * Ceil because we want to see a border if there is one
1649                 */
1650                $this->intrinsicWidth = DataType::toIntegerCeil($viewBoxWidth);
1651                return $this;
1652            } catch (ExceptionCompile $e) {
1653                throw new ExceptionBadSyntax("The media with ($viewBoxWidth) of the svg image ($this) is not a valid integer value");
1654            }
1655        }
1656
1657        /**
1658         * Case with some icon such as
1659         * https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs/fad-random-1dice.svg
1660         */
1661        $width = $this->getXmlDom()->documentElement->getAttribute("width");
1662        if ($width === "") {
1663            throw new ExceptionBadSyntax("The svg ($this) does not have a viewBox or width attribute, the intrinsic width cannot be determined");
1664        }
1665        try {
1666            $this->intrinsicWidth = DataType::toInteger($width);
1667            return $this;
1668        } catch (ExceptionCompile $e) {
1669            throw new ExceptionBadSyntax("The media width ($width) of the svg image ($this) is not a valid integer value");
1670        }
1671    }
1672
1673    /**
1674     * Build is done late because we want to be able to create a fetch url even if the file is not a correct svg
1675     *
1676     * The downside is that there is an exception that may be triggered all over the place
1677     *
1678     *
1679     * @throws ExceptionBadSyntax
1680     */
1681    private function buildXmlDocumentIfNeeded(string $markup = null): FetcherSvg
1682    {
1683        /**
1684         * The svg document may be build
1685         * via markup (See {@link self::setMarkup()}
1686         */
1687        if ($this->xmlDocument !== null) {
1688            return $this;
1689        }
1690
1691        /**
1692         * Markup string passed directly or
1693         * via the source path below
1694         */
1695        if ($markup !== null) {
1696            $this->xmlDocument = XmlDocument::createXmlDocFromMarkup($markup);
1697            $localName = $this->xmlDocument->getElement()->getLocalName();
1698            if ($localName !== "svg") {
1699                throw new ExceptionBadSyntax("This is not a svg but a $localName element.");
1700            }
1701            $this->setIntrinsicDimensions();
1702            return $this;
1703        }
1704
1705        /**
1706         * A svg path
1707         *
1708         * Because we test bad svg, we want to be able to build an url.
1709         * We don't want therefore to throw when the svg file is not valid
1710         * We therefore check the validity at runtime
1711         */
1712        $path = $this->getSourcePath();
1713        try {
1714            $markup = FileSystems::getContent($path);
1715        } catch (ExceptionNotFound $e) {
1716            throw new ExceptionRuntime("The svg file ($path) was not found", self::CANONICAL);
1717        }
1718        try {
1719            $this->buildXmlDocumentIfNeeded($markup);
1720        } catch (ExceptionBadSyntax $e) {
1721            throw new ExceptionRuntime("The svg file ($path) is not a valid svg. Error: {$e->getMessage()}");
1722        }
1723
1724        // dimension
1725        return $this;
1726
1727    }
1728
1729    /**
1730     * @return bool - true if the svg is an icon
1731     */
1732    public function isIconStructure(): bool
1733    {
1734        return $this->getInternalStructureType() === self::ICON_TYPE;
1735    }
1736
1737    /**
1738     * @return string - the internal structure of the svg
1739     * of {@link self::ICON_TYPE} or {@link self::ILLUSTRATION_TYPE}
1740     */
1741    private function getInternalStructureType(): string
1742    {
1743
1744        $mediaWidth = $this->getIntrinsicWidth();
1745        $mediaHeight = $this->getIntrinsicHeight();
1746
1747        if (
1748            $mediaWidth == $mediaHeight
1749            && $mediaWidth < 400) // 356 for logos telegram are the size of the twitter emoji but tile may be bigger ?
1750        {
1751            return FetcherSvg::ICON_TYPE;
1752        } else {
1753            $svgStructureType = FetcherSvg::ILLUSTRATION_TYPE;
1754
1755            // some icon may be bigger
1756            // in size than 400. example 1024 for ant-design:table-outlined
1757            // https://github.com/ant-design/ant-design-icons/blob/master/packages/icons-svg/svg/outlined/table.svg
1758            // or not squared
1759            // if the usage is determined or the svg is in the icon directory, it just takes over.
1760            try {
1761                $isInIconDirectory = IconDownloader::isInIconDirectory($this->getSourcePath());
1762            } catch (ExceptionNotFound $e) {
1763                // not a svg from a path
1764                $isInIconDirectory = false;
1765            }
1766            try {
1767                $requestType = $this->getRequestedType();
1768            } catch (ExceptionNotFound $e) {
1769                $requestType = false;
1770            }
1771
1772            if ($requestType === FetcherSvg::ICON_TYPE || $isInIconDirectory) {
1773                $svgStructureType = FetcherSvg::ICON_TYPE;
1774            }
1775
1776            return $svgStructureType;
1777
1778        }
1779    }
1780
1781    /**
1782     *
1783     * This function returns a consistent requested width and height for icon and tile
1784     *
1785     * @throws ExceptionNotFound - if not a icon or tile requested
1786     */
1787    private function getDefaultWidhtAndHeightForIconAndTileIfNotSet(): int
1788    {
1789
1790        if (!$this->norWidthNorHeightWasRequested()) {
1791            throw new ExceptionNotFound();
1792        }
1793
1794        if ($this->isCropRequested()) {
1795            /**
1796             * With a crop, the internal dimension takes over
1797             */
1798            throw new ExceptionNotFound();
1799        }
1800
1801        $internalStructure = $this->getInternalStructureType();
1802        switch ($internalStructure) {
1803            case FetcherSvg::ICON_TYPE:
1804                try {
1805                    $requestedType = $this->getRequestedType();
1806                } catch (ExceptionNotFound $e) {
1807                    $requestedType = FetcherSvg::ICON_TYPE;
1808                }
1809                switch ($requestedType) {
1810                    case FetcherSvg::TILE_TYPE:
1811                        return self::DEFAULT_TILE_WIDTH;
1812                    default:
1813                    case FetcherSvg::ICON_TYPE:
1814                        return FetcherSvg::DEFAULT_ICON_LENGTH;
1815                }
1816            default:
1817                throw new ExceptionNotFound();
1818        }
1819
1820
1821    }
1822
1823
1824}
1825