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