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