* */ namespace ComboStrap; use ComboStrap\TagAttribute\StyleAttribute; /** * Image * This is the class that handles the * svg link type */ class SvgImageLink extends ImageLink { const CANONICAL = FetcherSvg::CANONICAL; /** * Lazy Load */ const CONF_LAZY_LOAD_ENABLE = "svgLazyLoadEnable"; /** * Svg Injection */ const CONF_SVG_INJECTION_ENABLE = "svgInjectionEnable"; /** * Svg Injection Default * For now, there is a FOUC when the svg is visible, * The image does away, the layout shift, the image comes back, the layout shift * We disabled it by default then */ const CONF_SVG_INJECTION_ENABLE_DEFAULT = 0; const TAG = "svg"; /** * @throws ExceptionBadSyntax * @throws ExceptionBadArgument * @throws ExceptionNotExists */ public static function createFromFetcher(FetcherSvg $fetchImage) { return SvgImageLink::createFromMediaMarkup(MediaMarkup::createFromFetcher($fetchImage)); } /** * @throws ExceptionBadArgument * @throws ExceptionBadSyntax */ private function createImgHTMLTag(): string { $svgInjection = ExecutionContext::getActualOrCreateFromEnv() ->getConfig() ->getBooleanValue(self::CONF_SVG_INJECTION_ENABLE, self::CONF_SVG_INJECTION_ENABLE_DEFAULT); /** * Snippet */ $snippetManager = PluginUtility::getSnippetManager(); if ($svgInjection) { // Based on https://github.com/iconic/SVGInjector/ // See also: https://github.com/iconfu/svg-inject // !! There is a fork: https://github.com/tanem/svg-injector !! // Fallback ? : https://github.com/iconic/SVGInjector/#per-element-png-fallback $snippetManager ->attachRemoteJavascriptLibrary( "svg-injector", "https://cdn.jsdelivr.net/npm/svg-injector@1.1.3/dist/svg-injector.min.js", "sha256-CjBlJvxqLCU2HMzFunTelZLFHCJdqgDoHi/qGJWdRJk=" ) ->setDoesManipulateTheDomOnRun(false); } /** * Remove the cache attribute * (no cache for the img tag) * @var FetcherSvg $image */ $imgAttributes = $this->mediaMarkup->getExtraMediaTagAttributes() ->setLogicalTag(self::TAG); /** * Adaptive Image * It adds a `height: auto` that avoid a layout shift when * using the img tag */ $imgAttributes->addClassName(RasterImageLink::RESPONSIVE_CLASS); /** * Alt is mandatory */ $imgAttributes->addOutputAttributeValue("alt", $this->getAltNotEmpty()); /** * @var FetcherSvg $svgFetch */ $svgFetch = $this->mediaMarkup->getFetcher(); $srcValue = $svgFetch->getFetchUrl(); /** * Class management * */ $lazyLoad = $this->isLazyLoaded(); if ($lazyLoad) { // A class to all component lazy loaded to download them before print $imgAttributes->addClassName(LazyLoad::getLazyClass()); $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault(); switch ($lazyLoadMethod) { case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE: LazyLoad::addLozadSnippet(); if ($svgInjection) { $snippetManager->attachJavascriptFromComponentId("lozad-svg-injection"); $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg-injection")); } else { $snippetManager->attachJavascriptFromComponentId("lozad-svg"); $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("lazy-svg")); } /** * Note: Responsive image srcset is not needed for svg */ $imgAttributes->addOutputAttributeValue("data-src", $srcValue); $imgAttributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder( $svgFetch->getTargetWidth(), $svgFetch->getTargetHeight() )); break; case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE: $imgAttributes->addOutputAttributeValue(LazyLoad::HTML_LOADING_ATTRIBUTE, "lazy"); $imgAttributes->addOutputAttributeValue("src", $srcValue); break; } } else { if ($svgInjection) { $snippetManager->attachJavascriptFromComponentId("svg-injector"); $imgAttributes->addClassName(StyleAttribute::addComboStrapSuffix("svg-injection")); } $imgAttributes->addOutputAttributeValue("src", $srcValue); } /** * Dimension are mandatory on the image * to avoid layout shift (CLS) * We add them as output attribute */ $imgAttributes->addOutputAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getTargetWidth()); $imgAttributes->addOutputAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getTargetHeight()); /** * For styling, we add the width and height as component attribute */ try { $imgAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $svgFetch->getRequestedWidth()); } catch (ExceptionNotFound $e) { // ok } try { $imgAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $svgFetch->getRequestedHeight()); } catch (ExceptionNotFound $e) { // ok } /** * Return the image */ return $imgAttributes->toHtmlEmptyTag("img"); } /** * Render a link * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()} * A media can be a video also * @return string * @throws ExceptionNotFound * @throws ExceptionBadArgument */ public function renderMediaTag(): string { $imagePath = $this->mediaMarkup->getPath(); if (!FileSystems::exists($imagePath)) { throw new ExceptionNotFound("The image ($imagePath) does not exist"); } /** * TODO: Title/Label should be a node just below SVG */ $imageSize = FileSystems::getSize($imagePath); /** * Svg Style conflict: * when two svg are created and have a style node, they inject class * that may conflict with others (ie cls-1 class, ...) * The svg is then inserted via an img tag to scope it. */ try { $preserveStyle = DataType::toBoolean($this->mediaMarkup->getFetcher()->getFetchUrl()->getQueryPropertyValueAndRemoveIfPresent(FetcherSvg::REQUESTED_PRESERVE_ATTRIBUTE)); } catch (ExceptionNotFound $e) { $preserveStyle = false; } $asImgTag = $imageSize > $this->getMaxInlineSize() || $preserveStyle; if ($asImgTag) { /** * Img tag */ $imgHTML = $this->createImgHTMLTag(); } else { try { /** * Svg tag * @var FetcherSvg $fetcherSvg */ $fetcherSvg = $this->mediaMarkup->getFetcher(); try { $fetcherSvg->setRequestedClass($this->mediaMarkup->getExtraMediaTagAttributes()->getClass()); } catch (ExceptionNull $e) { // ok } $fetchPath = $fetcherSvg->getFetchPath(); $imgHTML = FileSystems::getContent($fetchPath); ExecutionContext::getActualOrCreateFromEnv() ->getSnippetSystem() ->attachCssInternalStyleSheet(DokuWiki::DOKUWIKI_STYLESHEET_ID); } catch (ExceptionNotFound|ExceptionBadArgument|ExceptionBadState|ExceptionBadSyntax|ExceptionCompile $e) { LogUtility::error("Unable to include the svg in the document. Error: {$e->getMessage()}"); $imgHTML = $this->createImgHTMLTag(); } } return $this->wrapMediaMarkupWithLink($imgHTML); } /** * @return int */ private function getMaxInlineSize() { return ExecutionContext::getActualOrCreateFromEnv() ->getConfig() ->getHtmlMaxInlineResourceSize(); } public function isLazyLoaded(): bool { if ($this->mediaMarkup->isLazy() === false) { return false; } return SiteConfig::getConfValue(self::CONF_LAZY_LOAD_ENABLE, 1); } }