xref: /plugin/combo/ComboStrap/SvgImageLink.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
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
15require_once(__DIR__ . '/MediaLink.php');
16require_once(__DIR__ . '/PluginUtility.php');
17require_once(__DIR__ . '/SvgDocument.php');
18
19/**
20 * Image
21 * This is the class that handles the
22 * svg link type
23 */
24class SvgImageLink extends MediaLink
25{
26
27    const CANONICAL = "svg";
28
29    /**
30     * The maximum size to be embedded
31     * Above this size limit they are fetched
32     */
33    const CONF_MAX_KB_SIZE_FOR_INLINE_SVG = "svgMaxInlineSizeKb";
34
35    /**
36     * Lazy Load
37     */
38    const CONF_LAZY_LOAD_ENABLE = "svgLazyLoadEnable";
39
40    /**
41     * Svg Injection
42     */
43    const CONF_SVG_INJECTION_ENABLE = "svgInjectionEnable";
44
45    /**
46     * @var SvgDocument
47     */
48    private $svgDocument;
49
50    /**
51     * SvgImageLink constructor.
52     * @param $ref
53     * @param TagAttributes $tagAttributes
54     * @param string $rev
55     */
56    public function __construct($ref, $tagAttributes = null, $rev = '')
57    {
58        parent::__construct($ref, $tagAttributes, $rev);
59        $this->getTagAttributes()->setLogicalTag(self::CANONICAL);
60    }
61
62
63    private function createImgHTMLTag()
64    {
65
66
67        $lazyLoad = $this->getLazyLoad();
68
69        $svgInjection = PluginUtility::getConfValue(self::CONF_SVG_INJECTION_ENABLE, 1);
70        /**
71         * Snippet
72         */
73        if ($svgInjection) {
74            $snippetManager = PluginUtility::getSnippetManager();
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->upsertTagsForBar("svg-injector",
81                array(
82                    'script' => [
83                        array(
84                            "src" => "https://cdn.jsdelivr.net/npm/svg-injector@1.1.3/svg-injector.min.js",
85                           // "integrity" => "sha256-CjBlJvxqLCU2HMzFunTelZLFHCJdqgDoHi/qGJWdRJk=",
86                            "crossorigin" => "anonymous"
87                        )
88                    ]
89                )
90            );
91        }
92
93        // Add lazy load snippet
94        if ($lazyLoad) {
95            LazyLoad::addLozadSnippet();
96        }
97
98        /**
99         * Remove the cache attribute
100         * (no cache for the img tag)
101         */
102        $this->tagAttributes->removeComponentAttributeIfPresent(CacheMedia::CACHE_KEY);
103
104        /**
105         * Remove linking (not yet implemented)
106         */
107        $this->tagAttributes->removeComponentAttributeIfPresent(MediaLink::LINKING_KEY);
108
109
110        /**
111         * Src
112         */
113        $srcValue = $this->getUrl();
114        if ($lazyLoad) {
115
116            /**
117             * Note: Responsive image srcset is not needed for svg
118             */
119            $this->tagAttributes->addHtmlAttributeValue("data-src", $srcValue);
120            $this->tagAttributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder($this->getImgTagWidthValue(), $this->getImgTagHeightValue()));
121
122        } else {
123
124            $this->tagAttributes->addHtmlAttributeValue("src", $srcValue);
125
126        }
127
128        /**
129         * Adaptive Image
130         * It adds a `height: auto` that avoid a layout shift when
131         * using the img tag
132         */
133        $this->tagAttributes->addClassName(RasterImageLink::RESPONSIVE_CLASS);
134
135
136        /**
137         * Title
138         */
139        if (!empty($this->getTitle())) {
140            $this->tagAttributes->addHtmlAttributeValue("alt", $this->getTitle());
141        }
142
143
144        /**
145         * Class management
146         *
147         * functionalClass is the class used in Javascript
148         * that should be in the class attribute
149         * When injected, the other class should come in a `data-class` attribute
150         */
151        $svgFunctionalClass = "";
152        if ($svgInjection && $lazyLoad) {
153            PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-svg-injection");
154            $svgFunctionalClass = "lazy-svg-injection-combo";
155        } else if ($lazyLoad && !$svgInjection) {
156            PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-svg");
157            $svgFunctionalClass = "lazy-svg-combo";
158        } else if ($svgInjection && !$lazyLoad) {
159            PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("svg-injector");
160            $svgFunctionalClass = "svg-injection-combo";
161        }
162        if ($lazyLoad) {
163            // A class to all component lazy loaded to download them before print
164            $svgFunctionalClass .= " " . LazyLoad::LAZY_CLASS;
165        }
166        $this->tagAttributes->addClassName($svgFunctionalClass);
167
168        /**
169         * Dimension are mandatory
170         * to avoid layout shift (CLS)
171         */
172        $this->tagAttributes->addHtmlAttributeValue(Dimension::WIDTH_KEY, $this->getImgTagWidthValue());
173        $this->tagAttributes->addHtmlAttributeValue(Dimension::HEIGHT_KEY, $this->getImgTagHeightValue());
174
175
176        /**
177         * Return the image
178         */
179        return '<img ' . $this->tagAttributes->toHTMLAttributeString() . '/>';
180
181    }
182
183
184    public function getAbsoluteUrl()
185    {
186
187        return $this->getUrl();
188
189    }
190
191    /**
192     * @param string $ampersand $absolute - the & separator (should be encoded for HTML but not for CSS)
193     * @return string|null
194     *
195     * At contrary to {@link RasterImageLink::getUrl()} this function does not need any width parameter
196     */
197    public function getUrl($ampersand = DokuwikiUrl::URL_ENCODED_AND)
198    {
199
200        if ($this->exists()) {
201
202            /**
203             * We remove align and linking because,
204             * they should apply only to the img tag
205             */
206
207
208            /**
209             *
210             * Create the array $att that will cary the query
211             * parameter for the URL
212             */
213            $att = array();
214            $componentAttributes = $this->tagAttributes->getComponentAttributes();
215            foreach ($componentAttributes as $name => $value) {
216
217                if (!in_array(strtolower($name), MediaLink::NON_URL_ATTRIBUTES)) {
218                    $newName = $name;
219
220                    /**
221                     * Width and Height
222                     * permits to create SVG of the asked size
223                     *
224                     * This is a little bit redundant with the
225                     * {@link Dimension::processWidthAndHeight()}
226                     * `max-width and width` styling property
227                     * but you may use them outside of HTML.
228                     */
229                    switch ($name) {
230                        case Dimension::WIDTH_KEY:
231                            $newName = "w";
232                            /**
233                             * We don't remove width because,
234                             * the sizing should apply to img
235                             */
236                            break;
237                        case Dimension::HEIGHT_KEY:
238                            $newName = "h";
239                            /**
240                             * We don't remove height because,
241                             * the sizing should apply to img
242                             */
243                            break;
244                    }
245
246                    if ($newName == CacheMedia::CACHE_KEY && $value == CacheMedia::CACHE_DEFAULT_VALUE) {
247                        // This is the default
248                        // No need to add it
249                        continue;
250                    }
251
252                    if (!empty($value)) {
253                        $att[$newName] = trim($value);
254                    }
255                }
256
257            }
258
259            /**
260             * Cache bursting
261             */
262            if (!$this->tagAttributes->hasComponentAttribute(CacheMedia::CACHE_BUSTER_KEY)) {
263                $att[CacheMedia::CACHE_BUSTER_KEY] = $this->getModifiedTime();
264            }
265
266            $direct = true;
267            return ml($this->getId(), $att, $direct, $ampersand, true);
268
269        } else {
270
271            return null;
272
273        }
274    }
275
276    /**
277     * Render a link
278     * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
279     * A media can be a video also
280     * @return string
281     */
282    public function renderMediaTag()
283    {
284
285        if ($this->exists()) {
286
287            /**
288             * This attributes should not be in the render
289             */
290            $this->tagAttributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE);
291            $this->tagAttributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC);
292            /**
293             * TODO: Title should be a node just below SVG
294             */
295            $this->tagAttributes->removeComponentAttributeIfPresent(Page::TITLE_META_PROPERTY);
296
297            if (
298                $this->getSize() > $this->getMaxInlineSize()
299            ) {
300
301                /**
302                 * Img tag
303                 */
304                $imgHTML = $this->createImgHTMLTag();
305
306            } else {
307
308                /**
309                 * Svg tag
310                 */
311                $imgHTML = file_get_contents($this->getSvgFile());
312
313            }
314
315
316        } else {
317
318            $imgHTML = "<span class=\"text-danger\">The svg ($this) does not exist</span>";
319
320        }
321        return $imgHTML;
322    }
323
324    private function getMaxInlineSize()
325    {
326        return PluginUtility::getConfValue(self::CONF_MAX_KB_SIZE_FOR_INLINE_SVG, 2) * 1024;
327    }
328
329
330    public function getLazyLoad()
331    {
332        $lazyLoad = parent::getLazyLoad();
333        if ($lazyLoad !== null) {
334            return $lazyLoad;
335        } else {
336            return PluginUtility::getConfValue(SvgImageLink::CONF_LAZY_LOAD_ENABLE);
337        }
338    }
339
340
341    public function getSvgFile()
342    {
343
344        $cache = new CacheMedia($this, $this->tagAttributes);
345        if (!$cache->isCacheUsable()) {
346            $content = $this->getSvgDocument()->getXmlText($this->tagAttributes);
347            $cache->storeCache($content);
348        }
349        return $cache->getFile()->getFileSystemPath();
350
351    }
352
353    public function getMediaWidth()
354    {
355        return $this->getSvgDocument()->getMediaWidth();
356    }
357
358    public function getMediaHeight()
359    {
360        return $this->getSvgDocument()->getMediaHeight();
361    }
362
363    private function getSvgDocument()
364    {
365        if ($this->svgDocument == null) {
366            $this->svgDocument = SvgDocument::createFromPath($this);
367        }
368        return $this->svgDocument;
369    }
370}
371