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