xref: /template/strap/ComboStrap/SvgImageLink.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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