1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15
16use ComboStrap\TagAttribute\StyleAttribute;
17
18/**
19 * Image
20 * This is the class that handles the
21 * svg link type
22 */
23class SvgImageLink extends ImageLink
24{
25
26    const CANONICAL = FetcherSvg::CANONICAL;
27
28    /**
29     * Lazy Load
30     */
31    const CONF_LAZY_LOAD_ENABLE = "svgLazyLoadEnable";
32
33    /**
34     * Svg Injection
35     */
36    const CONF_SVG_INJECTION_ENABLE = "svgInjectionEnable";
37    /**
38     * Svg Injection Default
39     * For now, there is a FOUC when the svg is visible,
40     * The image does away, the layout shift, the image comes back, the layout shift
41     * We disabled it by default then
42     */
43    const CONF_SVG_INJECTION_ENABLE_DEFAULT = 0;
44    const TAG = "svg";
45
46
47    /**
48     * @throws ExceptionBadSyntax
49     * @throws ExceptionBadArgument
50     * @throws ExceptionNotExists
51     */
52    public static function createFromFetcher(FetcherSvg $fetchImage)
53    {
54        return SvgImageLink::createFromMediaMarkup(MediaMarkup::createFromFetcher($fetchImage));
55    }
56
57
58    /**
59     * @throws ExceptionBadArgument
60     * @throws ExceptionBadSyntax
61     */
62    private function createImgHTMLTag(): string
63    {
64
65
66        $svgInjection = ExecutionContext::getActualOrCreateFromEnv()
67            ->getConfig()
68            ->getBooleanValue(self::CONF_SVG_INJECTION_ENABLE, self::CONF_SVG_INJECTION_ENABLE_DEFAULT);
69
70        /**
71         * Snippet
72         */
73        $snippetManager = PluginUtility::getSnippetManager();
74        if ($svgInjection) {
75
76            // Based on https://github.com/iconic/SVGInjector/
77            // See also: https://github.com/iconfu/svg-inject
78            // !! There is a fork: https://github.com/tanem/svg-injector !!
79            // Fallback ? : https://github.com/iconic/SVGInjector/#per-element-png-fallback
80            $snippetManager
81                ->attachRemoteJavascriptLibrary(
82                    "svg-injector",
83                    "https://cdn.jsdelivr.net/npm/svg-injector@1.1.3/dist/svg-injector.min.js",
84                    "sha256-CjBlJvxqLCU2HMzFunTelZLFHCJdqgDoHi/qGJWdRJk="
85                )
86                ->setDoesManipulateTheDomOnRun(false);
87
88        }
89
90
91        /**
92         * Remove the cache attribute
93         * (no cache for the img tag)
94         * @var FetcherSvg $image
95         */
96        $imgAttributes = $this->mediaMarkup->getExtraMediaTagAttributes()
97            ->setLogicalTag(self::TAG);
98
99        /**
100         * Adaptive Image
101         * It adds a `height: auto` that avoid a layout shift when
102         * using the img tag
103         */
104        $imgAttributes->addClassName(RasterImageLink::RESPONSIVE_CLASS);
105
106
107        /**
108         * Alt is mandatory
109         */
110        $imgAttributes->addOutputAttributeValue("alt", $this->getAltNotEmpty());
111
112
113        /**
114         * @var FetcherSvg $svgFetch
115         */
116        $svgFetch = $this->mediaMarkup->getFetcher();
117        $srcValue = $svgFetch->getFetchUrl();
118
119        /**
120         * Class management
121         *
122         */
123        $lazyLoad = $this->isLazyLoaded();
124        if ($lazyLoad) {
125            // A class to all component lazy loaded to download them before print
126            $imgAttributes->addClassName(LazyLoad::getLazyClass());
127            $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault();
128            switch ($lazyLoadMethod) {
129                case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE:
130                    LazyLoad::addLozadSnippet();
131                    if ($svgInjection) {
132                        $snippetManager->attachJavascriptFromComponentId("lozad-svg-injection");
133                        $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg-injection"));
134                    } else {
135                        $snippetManager->attachJavascriptFromComponentId("lozad-svg");
136                        $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg"));
137                    }
138                    /**
139                     * Note: Responsive image srcset is not needed for svg
140                     */
141                    $imgAttributes->addOutputAttributeValue("data-src", $srcValue);
142                    $imgAttributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder(
143                        $svgFetch->getTargetWidth(),
144                        $svgFetch->getTargetHeight()
145                    ));
146                    break;
147                case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE:
148                    $imgAttributes->addOutputAttributeValue(LazyLoad::HTML_LOADING_ATTRIBUTE, "lazy");
149                    $imgAttributes->addOutputAttributeValue("src", $srcValue);
150                    break;
151            }
152
153        } else {
154            if ($svgInjection) {
155                $snippetManager->attachJavascriptFromComponentId("svg-injector");
156                $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("svg-injection"));
157            }
158            $imgAttributes->addOutputAttributeValue("src", $srcValue);
159        }
160
161
162
163        /**
164         * Dimension are mandatory on the image
165         * to avoid layout shift (CLS)
166         * We add them as output attribute
167         */
168        $imgAttributes->addOutputAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getTargetWidth());
169        $imgAttributes->addOutputAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getTargetHeight());
170
171        /**
172         * For styling, we add the width and height as component attribute
173         */
174        try {
175            $imgAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getRequestedWidth());
176        } catch (ExceptionNotFound $e) {
177            // ok
178        }
179        try {
180            $imgAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getRequestedHeight());
181        } catch (ExceptionNotFound $e) {
182            // ok
183        }
184
185        /**
186         * Return the image
187         */
188        return $imgAttributes->toHtmlEmptyTag("img");
189
190
191    }
192
193
194    /**
195     * Render a link
196     * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
197     * A media can be a video also
198     * @return string
199     * @throws ExceptionNotFound
200     * @throws ExceptionBadArgument
201     */
202    public function renderMediaTag(): string
203    {
204
205
206        $imagePath = $this->mediaMarkup->getPath();
207        if (!FileSystems::exists($imagePath)) {
208            throw new ExceptionNotFound("The image ($imagePath) does not exist");
209        }
210
211        /**
212         * TODO: Title/Label should be a node just below SVG
213         */
214        $imageSize = FileSystems::getSize($imagePath);
215
216        /**
217         * Svg Style conflict:
218         * when two svg are created and have a style node, they inject class
219         * that may conflict with others (ie cls-1 class, ...)
220         * The svg is then inserted via an img tag to scope it.
221         */
222        try {
223            $preserveStyle = DataType::toBoolean($this->mediaMarkup->getFetcher()->getFetchUrl()->getQueryPropertyValueAndRemoveIfPresent(FetcherSvg::REQUESTED_PRESERVE_ATTRIBUTE));
224        } catch (ExceptionNotFound $e) {
225            $preserveStyle = false;
226        }
227
228        $asImgTag = $imageSize > $this->getMaxInlineSize() || $preserveStyle;
229        if ($asImgTag) {
230
231            /**
232             * Img tag
233             */
234            $imgHTML = $this->createImgHTMLTag();
235
236        } else {
237
238
239            try {
240                /**
241                 * Svg tag
242                 * @var FetcherSvg $fetcherSvg
243                 */
244                $fetcherSvg = $this->mediaMarkup->getFetcher();
245                try {
246                    $fetcherSvg->setRequestedClass($this->mediaMarkup->getExtraMediaTagAttributes()->getClass());
247                } catch (ExceptionNull $e) {
248                    // ok
249                }
250                $fetchPath = $fetcherSvg->getFetchPath();
251                $imgHTML = FileSystems::getContent($fetchPath);
252                ExecutionContext::getActualOrCreateFromEnv()
253                    ->getSnippetSystem()
254                    ->attachCssInternalStyleSheet(DokuWiki::DOKUWIKI_STYLESHEET_ID);
255            } catch (ExceptionNotFound|ExceptionBadArgument|ExceptionBadState|ExceptionBadSyntax|ExceptionCompile $e) {
256                LogUtility::error("Unable to include the svg in the document. Error: {$e->getMessage()}");
257                $imgHTML = $this->createImgHTMLTag();
258            }
259
260        }
261
262        return $this->wrapMediaMarkupWithLink($imgHTML);
263
264    }
265
266    /**
267     * @return int
268     */
269    private function getMaxInlineSize()
270    {
271        return ExecutionContext::getActualOrCreateFromEnv()
272            ->getConfig()
273            ->getHtmlMaxInlineResourceSize();
274    }
275
276
277    public function isLazyLoaded(): bool
278    {
279
280        if ($this->mediaMarkup->isLazy() === false) {
281            return false;
282        }
283        return SiteConfig::getConfValue(self::CONF_LAZY_LOAD_ENABLE, 1);
284
285    }
286
287}
288