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