137748cd8SNickeau<?php 237748cd8SNickeau/** 337748cd8SNickeau * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved. 437748cd8SNickeau * 537748cd8SNickeau * This source code is licensed under the GPL license found in the 637748cd8SNickeau * COPYING file in the root directory of this source tree. 737748cd8SNickeau * 837748cd8SNickeau * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 937748cd8SNickeau * @author ComboStrap <support@combostrap.com> 1037748cd8SNickeau * 1137748cd8SNickeau */ 1237748cd8SNickeau 1337748cd8SNickeaunamespace ComboStrap; 1437748cd8SNickeau 151fa8c418SNickeau 16*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 1737748cd8SNickeau 1837748cd8SNickeau/** 1937748cd8SNickeau * Image 2037748cd8SNickeau * This is the class that handles the 2137748cd8SNickeau * svg link type 2237748cd8SNickeau */ 231fa8c418SNickeauclass SvgImageLink extends ImageLink 2437748cd8SNickeau{ 2537748cd8SNickeau 26*04fd306cSNickeau const CANONICAL = FetcherSvg::CANONICAL; 2737748cd8SNickeau 2837748cd8SNickeau /** 2937748cd8SNickeau * Lazy Load 3037748cd8SNickeau */ 3137748cd8SNickeau const CONF_LAZY_LOAD_ENABLE = "svgLazyLoadEnable"; 3237748cd8SNickeau 3337748cd8SNickeau /** 3437748cd8SNickeau * Svg Injection 3537748cd8SNickeau */ 3637748cd8SNickeau const CONF_SVG_INJECTION_ENABLE = "svgInjectionEnable"; 37*04fd306cSNickeau /** 38*04fd306cSNickeau * Svg Injection Default 39*04fd306cSNickeau * For now, there is a FOUC when the svg is visible, 40*04fd306cSNickeau * The image does away, the layout shift, the image comes back, the layout shift 41*04fd306cSNickeau * We disabled it by default then 42*04fd306cSNickeau */ 43*04fd306cSNickeau const CONF_SVG_INJECTION_ENABLE_DEFAULT = 0; 44*04fd306cSNickeau const TAG = "svg"; 4537748cd8SNickeau 4637748cd8SNickeau 4737748cd8SNickeau /** 48*04fd306cSNickeau * @throws ExceptionBadSyntax 49*04fd306cSNickeau * @throws ExceptionBadArgument 50*04fd306cSNickeau * @throws ExceptionNotExists 5137748cd8SNickeau */ 52*04fd306cSNickeau public static function createFromFetcher(FetcherSvg $fetchImage) 5337748cd8SNickeau { 54*04fd306cSNickeau return SvgImageLink::createFromMediaMarkup(MediaMarkup::createFromFetcher($fetchImage)); 5537748cd8SNickeau } 5637748cd8SNickeau 5737748cd8SNickeau 584cadd4f8SNickeau /** 59*04fd306cSNickeau * @throws ExceptionBadArgument 60*04fd306cSNickeau * @throws ExceptionBadSyntax 614cadd4f8SNickeau */ 621fa8c418SNickeau private function createImgHTMLTag(): string 6337748cd8SNickeau { 6437748cd8SNickeau 6537748cd8SNickeau 66*04fd306cSNickeau $svgInjection = ExecutionContext::getActualOrCreateFromEnv() 67*04fd306cSNickeau ->getConfig() 68*04fd306cSNickeau ->getBooleanValue(self::CONF_SVG_INJECTION_ENABLE, self::CONF_SVG_INJECTION_ENABLE_DEFAULT); 6937748cd8SNickeau 7037748cd8SNickeau /** 7137748cd8SNickeau * Snippet 7237748cd8SNickeau */ 7337748cd8SNickeau $snippetManager = PluginUtility::getSnippetManager(); 744cadd4f8SNickeau if ($svgInjection) { 7537748cd8SNickeau 7637748cd8SNickeau // Based on https://github.com/iconic/SVGInjector/ 7737748cd8SNickeau // See also: https://github.com/iconfu/svg-inject 7837748cd8SNickeau // !! There is a fork: https://github.com/tanem/svg-injector !! 7937748cd8SNickeau // Fallback ? : https://github.com/iconic/SVGInjector/#per-element-png-fallback 804cadd4f8SNickeau $snippetManager 81*04fd306cSNickeau ->attachRemoteJavascriptLibrary( 824cadd4f8SNickeau "svg-injector", 834cadd4f8SNickeau "https://cdn.jsdelivr.net/npm/svg-injector@1.1.3/dist/svg-injector.min.js", 844cadd4f8SNickeau "sha256-CjBlJvxqLCU2HMzFunTelZLFHCJdqgDoHi/qGJWdRJk=" 8537748cd8SNickeau ) 864cadd4f8SNickeau ->setDoesManipulateTheDomOnRun(false); 874cadd4f8SNickeau 8837748cd8SNickeau } 8937748cd8SNickeau 9037748cd8SNickeau 9137748cd8SNickeau /** 9237748cd8SNickeau * Remove the cache attribute 9337748cd8SNickeau * (no cache for the img tag) 94*04fd306cSNickeau * @var FetcherSvg $image 9537748cd8SNickeau */ 96*04fd306cSNickeau $imgAttributes = $this->mediaMarkup->getExtraMediaTagAttributes() 97*04fd306cSNickeau ->setLogicalTag(self::TAG); 9837748cd8SNickeau 9937748cd8SNickeau /** 10037748cd8SNickeau * Adaptive Image 10137748cd8SNickeau * It adds a `height: auto` that avoid a layout shift when 10237748cd8SNickeau * using the img tag 10337748cd8SNickeau */ 104*04fd306cSNickeau $imgAttributes->addClassName(RasterImageLink::RESPONSIVE_CLASS); 10537748cd8SNickeau 10637748cd8SNickeau 10737748cd8SNickeau /** 1081fa8c418SNickeau * Alt is mandatory 10937748cd8SNickeau */ 110*04fd306cSNickeau $imgAttributes->addOutputAttributeValue("alt", $this->getAltNotEmpty()); 11137748cd8SNickeau 11237748cd8SNickeau 11337748cd8SNickeau /** 114*04fd306cSNickeau * @var FetcherSvg $svgFetch 115*04fd306cSNickeau */ 116*04fd306cSNickeau $svgFetch = $this->mediaMarkup->getFetcher(); 117*04fd306cSNickeau $srcValue = $svgFetch->getFetchUrl(); 118*04fd306cSNickeau 119*04fd306cSNickeau /** 12037748cd8SNickeau * Class management 12137748cd8SNickeau * 12237748cd8SNickeau */ 123*04fd306cSNickeau $lazyLoad = $this->isLazyLoaded(); 12437748cd8SNickeau if ($lazyLoad) { 12537748cd8SNickeau // A class to all component lazy loaded to download them before print 126*04fd306cSNickeau $imgAttributes->addClassName(LazyLoad::getLazyClass()); 127*04fd306cSNickeau $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault(); 128*04fd306cSNickeau switch ($lazyLoadMethod) { 129*04fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE: 130*04fd306cSNickeau LazyLoad::addLozadSnippet(); 131*04fd306cSNickeau if ($svgInjection) { 132*04fd306cSNickeau $snippetManager->attachJavascriptFromComponentId("lozad-svg-injection"); 133*04fd306cSNickeau $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg-injection")); 134*04fd306cSNickeau } else { 135*04fd306cSNickeau $snippetManager->attachJavascriptFromComponentId("lozad-svg"); 136*04fd306cSNickeau $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg")); 13737748cd8SNickeau } 138c3437056SNickeau /** 139c3437056SNickeau * Note: Responsive image srcset is not needed for svg 140c3437056SNickeau */ 141*04fd306cSNickeau $imgAttributes->addOutputAttributeValue("data-src", $srcValue); 142*04fd306cSNickeau $imgAttributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder( 143*04fd306cSNickeau $svgFetch->getTargetWidth(), 144*04fd306cSNickeau $svgFetch->getTargetHeight() 145c3437056SNickeau )); 146*04fd306cSNickeau break; 147*04fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE: 148*04fd306cSNickeau $imgAttributes->addOutputAttributeValue(LazyLoad::HTML_LOADING_ATTRIBUTE, "lazy"); 149*04fd306cSNickeau $imgAttributes->addOutputAttributeValue("src", $srcValue); 150*04fd306cSNickeau break; 151c3437056SNickeau } 152c3437056SNickeau 153*04fd306cSNickeau } else { 154*04fd306cSNickeau if ($svgInjection) { 155*04fd306cSNickeau $snippetManager->attachJavascriptFromComponentId("svg-injector"); 156*04fd306cSNickeau $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("svg-injection")); 157*04fd306cSNickeau } 158*04fd306cSNickeau $imgAttributes->addOutputAttributeValue("src", $srcValue); 159*04fd306cSNickeau } 160*04fd306cSNickeau 161*04fd306cSNickeau 16237748cd8SNickeau 16337748cd8SNickeau /** 164*04fd306cSNickeau * Dimension are mandatory on the image 165*04fd306cSNickeau * to avoid layout shift (CLS) 166*04fd306cSNickeau * We add them as output attribute 16782a60d03SNickeau */ 168*04fd306cSNickeau $imgAttributes->addOutputAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getTargetWidth()); 169*04fd306cSNickeau $imgAttributes->addOutputAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getTargetHeight()); 170*04fd306cSNickeau 171*04fd306cSNickeau /** 172*04fd306cSNickeau * For styling, we add the width and height as component attribute 173*04fd306cSNickeau */ 174*04fd306cSNickeau try { 175*04fd306cSNickeau $imgAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getRequestedWidth()); 176*04fd306cSNickeau } catch (ExceptionNotFound $e) { 177*04fd306cSNickeau // ok 178*04fd306cSNickeau } 179*04fd306cSNickeau try { 180*04fd306cSNickeau $imgAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getRequestedHeight()); 181*04fd306cSNickeau } catch (ExceptionNotFound $e) { 182*04fd306cSNickeau // ok 183*04fd306cSNickeau } 18482a60d03SNickeau 18582a60d03SNickeau /** 18637748cd8SNickeau * Return the image 18737748cd8SNickeau */ 188*04fd306cSNickeau return $imgAttributes->toHtmlEmptyTag("img"); 189*04fd306cSNickeau 19037748cd8SNickeau 19137748cd8SNickeau } 19237748cd8SNickeau 19337748cd8SNickeau 19437748cd8SNickeau /** 19537748cd8SNickeau * Render a link 19637748cd8SNickeau * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()} 19737748cd8SNickeau * A media can be a video also 19837748cd8SNickeau * @return string 199*04fd306cSNickeau * @throws ExceptionNotFound 200*04fd306cSNickeau * @throws ExceptionBadArgument 20137748cd8SNickeau */ 2021fa8c418SNickeau public function renderMediaTag(): string 20337748cd8SNickeau { 20437748cd8SNickeau 205*04fd306cSNickeau 206*04fd306cSNickeau $imagePath = $this->mediaMarkup->getPath(); 207*04fd306cSNickeau if (!FileSystems::exists($imagePath)) { 208*04fd306cSNickeau throw new ExceptionNotFound("The image ($imagePath) does not exist"); 209*04fd306cSNickeau } 21037748cd8SNickeau 21137748cd8SNickeau /** 212*04fd306cSNickeau * TODO: Title/Label should be a node just below SVG 21337748cd8SNickeau */ 214*04fd306cSNickeau $imageSize = FileSystems::getSize($imagePath); 21537748cd8SNickeau 216*04fd306cSNickeau /** 217*04fd306cSNickeau * Svg Style conflict: 218*04fd306cSNickeau * when two svg are created and have a style node, they inject class 219*04fd306cSNickeau * that may conflict with others (ie cls-1 class, ...) 220*04fd306cSNickeau * The svg is then inserted via an img tag to scope it. 221*04fd306cSNickeau */ 222*04fd306cSNickeau try { 223*04fd306cSNickeau $preserveStyle = DataType::toBoolean($this->mediaMarkup->getFetcher()->getFetchUrl()->getQueryPropertyValueAndRemoveIfPresent(FetcherSvg::REQUESTED_PRESERVE_ATTRIBUTE)); 224*04fd306cSNickeau } catch (ExceptionNotFound $e) { 225*04fd306cSNickeau $preserveStyle = false; 226*04fd306cSNickeau } 227*04fd306cSNickeau 228*04fd306cSNickeau $asImgTag = $imageSize > $this->getMaxInlineSize() || $preserveStyle; 229*04fd306cSNickeau if ($asImgTag) { 23037748cd8SNickeau 23137748cd8SNickeau /** 23237748cd8SNickeau * Img tag 23337748cd8SNickeau */ 23437748cd8SNickeau $imgHTML = $this->createImgHTMLTag(); 23537748cd8SNickeau 23637748cd8SNickeau } else { 23737748cd8SNickeau 238*04fd306cSNickeau 239*04fd306cSNickeau try { 24037748cd8SNickeau /** 24137748cd8SNickeau * Svg tag 242*04fd306cSNickeau * @var FetcherSvg $fetcherSvg 24337748cd8SNickeau */ 244*04fd306cSNickeau $fetcherSvg = $this->mediaMarkup->getFetcher(); 2454cadd4f8SNickeau try { 246*04fd306cSNickeau $fetcherSvg->setRequestedClass($this->mediaMarkup->getExtraMediaTagAttributes()->getClass()); 247*04fd306cSNickeau } catch (ExceptionNull $e) { 248*04fd306cSNickeau // ok 249*04fd306cSNickeau } 250*04fd306cSNickeau $fetchPath = $fetcherSvg->getFetchPath(); 251*04fd306cSNickeau $imgHTML = FileSystems::getContent($fetchPath); 252*04fd306cSNickeau ExecutionContext::getActualOrCreateFromEnv() 253*04fd306cSNickeau ->getSnippetSystem() 254*04fd306cSNickeau ->attachCssInternalStyleSheet(DokuWiki::DOKUWIKI_STYLESHEET_ID); 255*04fd306cSNickeau } catch (ExceptionNotFound|ExceptionBadArgument|ExceptionBadState|ExceptionBadSyntax|ExceptionCompile $e) { 256*04fd306cSNickeau LogUtility::error("Unable to include the svg in the document. Error: {$e->getMessage()}"); 257*04fd306cSNickeau $imgHTML = $this->createImgHTMLTag(); 2584cadd4f8SNickeau } 25937748cd8SNickeau 26037748cd8SNickeau } 26137748cd8SNickeau 262*04fd306cSNickeau return $this->wrapMediaMarkupWithLink($imgHTML); 26337748cd8SNickeau 26437748cd8SNickeau } 26537748cd8SNickeau 266*04fd306cSNickeau /** 267*04fd306cSNickeau * @return int 268*04fd306cSNickeau */ 26937748cd8SNickeau private function getMaxInlineSize() 27037748cd8SNickeau { 271*04fd306cSNickeau return ExecutionContext::getActualOrCreateFromEnv() 272*04fd306cSNickeau ->getConfig() 273*04fd306cSNickeau ->getHtmlMaxInlineResourceSize(); 27437748cd8SNickeau } 27537748cd8SNickeau 27637748cd8SNickeau 277*04fd306cSNickeau public function isLazyLoaded(): bool 27837748cd8SNickeau { 27937748cd8SNickeau 280*04fd306cSNickeau if ($this->mediaMarkup->isLazy() === false) { 281*04fd306cSNickeau return false; 282*04fd306cSNickeau } 283*04fd306cSNickeau return SiteConfig::getConfValue(self::CONF_LAZY_LOAD_ENABLE, 1); 284*04fd306cSNickeau 285*04fd306cSNickeau } 28637748cd8SNickeau 28737748cd8SNickeau} 288