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