xref: /plugin/combo/ComboStrap/RasterImageLink.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
15*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
16*04fd306cSNickeau
1737748cd8SNickeaurequire_once(__DIR__ . '/MediaLink.php');
1837748cd8SNickeaurequire_once(__DIR__ . '/LazyLoad.php');
1937748cd8SNickeaurequire_once(__DIR__ . '/PluginUtility.php');
2037748cd8SNickeau
2137748cd8SNickeau/**
2237748cd8SNickeau * Image
2337748cd8SNickeau * This is the class that handles the
2437748cd8SNickeau * raster image type of the dokuwiki {@link MediaLink}
2537748cd8SNickeau *
2637748cd8SNickeau * The real documentation can be found on the image page
2737748cd8SNickeau * @link https://www.dokuwiki.org/images
2837748cd8SNickeau *
2937748cd8SNickeau * Doc:
3037748cd8SNickeau * https://web.dev/optimize-cls/#images-without-dimensions
3137748cd8SNickeau * https://web.dev/cls/
3237748cd8SNickeau */
331fa8c418SNickeauclass RasterImageLink extends ImageLink
3437748cd8SNickeau{
3537748cd8SNickeau
36*04fd306cSNickeau    const CANONICAL = FetcherRaster::CANONICAL;
3737748cd8SNickeau
3837748cd8SNickeau    const RESPONSIVE_CLASS = "img-fluid";
3937748cd8SNickeau
4037748cd8SNickeau    const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin";
4137748cd8SNickeau    const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable";
4237748cd8SNickeau
43*04fd306cSNickeau    private FetcherImage $fetchRaster;
4437748cd8SNickeau
4537748cd8SNickeau
4637748cd8SNickeau    /**
47*04fd306cSNickeau     * @throws ExceptionBadArgument - if the fetcher is not a raster mime and image fetcher
4837748cd8SNickeau     */
49*04fd306cSNickeau    public function __construct(MediaMarkup $mediaMarkup)
5037748cd8SNickeau    {
51*04fd306cSNickeau        $fetcher = $mediaMarkup->getFetcher();
52*04fd306cSNickeau        $mime = $fetcher->getMime();
53*04fd306cSNickeau        if (!$mime->isSupportedRasterImage()) {
54*04fd306cSNickeau            throw new ExceptionBadArgument("The mime value ($mime) is not a supported raster image.", self::CANONICAL);
55*04fd306cSNickeau        }
56*04fd306cSNickeau        if (!($fetcher instanceof FetcherImage)) {
57*04fd306cSNickeau            throw new ExceptionBadArgument("The fetcher is not a fetcher image but is a " . get_class($fetcher));
58*04fd306cSNickeau        }
59*04fd306cSNickeau        $this->fetchRaster = $fetcher;
60*04fd306cSNickeau        parent::__construct($mediaMarkup);
6137748cd8SNickeau    }
6237748cd8SNickeau
6337748cd8SNickeau
6437748cd8SNickeau    /**
6537748cd8SNickeau     * Render a link
6637748cd8SNickeau     * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
6737748cd8SNickeau     * A media can be a video also (Use
6837748cd8SNickeau     * @return string
69*04fd306cSNickeau     * @throws ExceptionNotFound
7037748cd8SNickeau     */
711fa8c418SNickeau    public function renderMediaTag(): string
7237748cd8SNickeau    {
7337748cd8SNickeau
7437748cd8SNickeau
75*04fd306cSNickeau        $fetchRaster = $this->fetchRaster;
76*04fd306cSNickeau
77*04fd306cSNickeau        $attributes = $this->mediaMarkup->getExtraMediaTagAttributes()
78*04fd306cSNickeau            ->setLogicalTag(self::CANONICAL);
7937748cd8SNickeau
8037748cd8SNickeau        /**
8137748cd8SNickeau         * Responsive image
8237748cd8SNickeau         * https://getbootstrap.com/docs/5.0/content/images/
8337748cd8SNickeau         * to apply max-width: 100%; and height: auto;
8437748cd8SNickeau         *
8537748cd8SNickeau         * Even if the resizing is requested by height,
8637748cd8SNickeau         * the height: auto on styling is needed to conserve the ratio
8737748cd8SNickeau         * while scaling down the screen
8837748cd8SNickeau         */
891fa8c418SNickeau        $attributes->addClassName(self::RESPONSIVE_CLASS);
9037748cd8SNickeau
9137748cd8SNickeau
9237748cd8SNickeau        /**
9337748cd8SNickeau         * width and height to give the dimension ratio
9437748cd8SNickeau         * They have an effect on the space reservation
9537748cd8SNickeau         * but not on responsive image at all
9637748cd8SNickeau         * To allow responsive height, the height style property is set at auto
9737748cd8SNickeau         * (ie img-fluid in bootstrap)
9837748cd8SNickeau         */
9937748cd8SNickeau        // The unit is not mandatory in HTML, this is expected to be CSS pixel
10037748cd8SNickeau        // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
10137748cd8SNickeau        // The HTML validator does not expect an unit otherwise it send an error
10237748cd8SNickeau        // https://validator.w3.org/
10337748cd8SNickeau        $htmlLengthUnit = "";
10482a60d03SNickeau        $cssLengthUnit = "px";
10537748cd8SNickeau
10637748cd8SNickeau        /**
10737748cd8SNickeau         * Height
10837748cd8SNickeau         * The logical height that the image should take on the page
10937748cd8SNickeau         *
11037748cd8SNickeau         * Note: The style is also set in {@link Dimension::processWidthAndHeight()}
11137748cd8SNickeau         *
112*04fd306cSNickeau         * Cannot be empty
11337748cd8SNickeau         */
114*04fd306cSNickeau        $targetHeight = $fetchRaster->getTargetHeight();
115*04fd306cSNickeau
11682a60d03SNickeau        /**
11782a60d03SNickeau         * HTML height attribute is important for the ratio calculation
11882a60d03SNickeau         * No layout shift
11982a60d03SNickeau         */
1204cadd4f8SNickeau        $attributes->addOutputAttributeValue("height", $targetHeight . $htmlLengthUnit);
12182a60d03SNickeau        /**
12282a60d03SNickeau         * We don't allow the image to scale up by default
12382a60d03SNickeau         */
12482a60d03SNickeau        $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit);
1254cadd4f8SNickeau        /**
1264cadd4f8SNickeau         * if the image has a class that has a `height: 100%`, the image will stretch
1274cadd4f8SNickeau         */
1284cadd4f8SNickeau        $attributes->addStyleDeclarationIfNotSet("height", "auto");
12937748cd8SNickeau
13037748cd8SNickeau
13137748cd8SNickeau        /**
13237748cd8SNickeau         * Responsive image src set building
13337748cd8SNickeau         * We have chosen
13437748cd8SNickeau         *   * 375: Iphone6
13537748cd8SNickeau         *   * 768: Ipad
13637748cd8SNickeau         *   * 1024: Ipad Pro
13737748cd8SNickeau         *
13837748cd8SNickeau         */
13937748cd8SNickeau        // The image margin applied
140*04fd306cSNickeau        $imageMargin = SiteConfig::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px");
14137748cd8SNickeau
14237748cd8SNickeau
14337748cd8SNickeau        /**
14437748cd8SNickeau         * Srcset and sizes for responsive image
14537748cd8SNickeau         * Width is mandatory for responsive image
14637748cd8SNickeau         * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images
14737748cd8SNickeau         */
148*04fd306cSNickeau
14937748cd8SNickeau
15037748cd8SNickeau        /**
1511fa8c418SNickeau         * The value of the target image
15237748cd8SNickeau         */
153*04fd306cSNickeau        $targetWidth = $fetchRaster->getTargetWidth();
154*04fd306cSNickeau        $fetchRaster->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight);
15537748cd8SNickeau
15682a60d03SNickeau        /**
15782a60d03SNickeau         * HTML Width attribute is important to avoid layout shift
15882a60d03SNickeau         */
1594cadd4f8SNickeau        $attributes->addOutputAttributeValue("width", $targetWidth . $htmlLengthUnit);
16082a60d03SNickeau        /**
16182a60d03SNickeau         * We don't allow the image to scale up by default
16282a60d03SNickeau         */
16382a60d03SNickeau        $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit);
16482a60d03SNickeau        /**
16582a60d03SNickeau         * We allow the image to scale down up to 100% of its parent
16682a60d03SNickeau         */
16782a60d03SNickeau        $attributes->addStyleDeclarationIfNotSet("width", "100%");
16882a60d03SNickeau
16937748cd8SNickeau
17037748cd8SNickeau        /**
17137748cd8SNickeau         * Continue
17237748cd8SNickeau         */
17337748cd8SNickeau        $srcSet = "";
17437748cd8SNickeau        $sizes = "";
17537748cd8SNickeau
17637748cd8SNickeau        /**
177*04fd306cSNickeau         * Width
178*04fd306cSNickeau         *
179*04fd306cSNickeau         * We create a series of URL
180*04fd306cSNickeau         * for different width and let the browser
181*04fd306cSNickeau         * download the best one for:
182*04fd306cSNickeau         *   * the actual container width
183*04fd306cSNickeau         *   * the actual of screen resolution
184*04fd306cSNickeau         *   * and the connection speed.
185*04fd306cSNickeau         *
186*04fd306cSNickeau         * The max-width value is set
18737748cd8SNickeau         */
188*04fd306cSNickeau        $srcValue = $fetchRaster->getFetchUrl();
189*04fd306cSNickeau        /**
190*04fd306cSNickeau         * Add samller breakpoints sizes
191*04fd306cSNickeau         */
192*04fd306cSNickeau        $intrinsicWidth = $fetchRaster->getIntrinsicWidth();
193*04fd306cSNickeau        foreach (Breakpoint::getBreakpoints() as $breakpoint) {
19437748cd8SNickeau
195*04fd306cSNickeau            try {
196*04fd306cSNickeau                $breakpointPixels = $breakpoint->getWidth();
197*04fd306cSNickeau            } catch (ExceptionInfinite $e) {
198*04fd306cSNickeau                continue;
199*04fd306cSNickeau            }
200*04fd306cSNickeau
201*04fd306cSNickeau            if ($breakpointPixels > $targetWidth) {
202*04fd306cSNickeau                continue;
203*04fd306cSNickeau            }
204*04fd306cSNickeau
205*04fd306cSNickeau            if ($breakpointPixels > $intrinsicWidth) {
206*04fd306cSNickeau                continue;
207*04fd306cSNickeau            }
20837748cd8SNickeau
20937748cd8SNickeau            if (!empty($srcSet)) {
21037748cd8SNickeau                $srcSet .= ", ";
21137748cd8SNickeau                $sizes .= ", ";
21237748cd8SNickeau            }
213*04fd306cSNickeau            $breakpointWidthMinusMargin = $breakpointPixels - $imageMargin;
2144cadd4f8SNickeau
21537748cd8SNickeau
216*04fd306cSNickeau            $breakpointRaster = clone $fetchRaster;
217*04fd306cSNickeau            if (
218*04fd306cSNickeau                !$fetchRaster->hasHeightRequested() // breakpoint url needs only the h attribute in this case
219*04fd306cSNickeau                || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory
220*04fd306cSNickeau            ) {
221*04fd306cSNickeau                $breakpointRaster->setRequestedWidth($breakpointWidthMinusMargin);
22237748cd8SNickeau            }
223*04fd306cSNickeau            if ($fetchRaster->hasHeightRequested() // if this is a height request
224*04fd306cSNickeau                || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory
225*04fd306cSNickeau            ) {
226*04fd306cSNickeau                $breakPointHeight = FetcherRaster::round($breakpointWidthMinusMargin / $fetchRaster->getTargetAspectRatio());
227*04fd306cSNickeau                $breakpointRaster->setRequestedHeight($breakPointHeight);
228*04fd306cSNickeau            }
229*04fd306cSNickeau
230*04fd306cSNickeau            $breakpointUrl = $breakpointRaster->getFetchUrl()->toString();
231*04fd306cSNickeau
232*04fd306cSNickeau
233*04fd306cSNickeau            $srcSet .= "$breakpointUrl {$breakpointWidthMinusMargin}w";
234*04fd306cSNickeau            $sizes .= $this->getSizes($breakpointPixels, $breakpointWidthMinusMargin);
235*04fd306cSNickeau
23637748cd8SNickeau
23737748cd8SNickeau        }
23837748cd8SNickeau
23937748cd8SNickeau        /**
24037748cd8SNickeau         * Add the last size
2411fa8c418SNickeau         * If the target image is really small, srcset and sizes are empty
24237748cd8SNickeau         */
24337748cd8SNickeau        if (!empty($srcSet)) {
24437748cd8SNickeau            $srcSet .= ", ";
24537748cd8SNickeau            $sizes .= ", ";
246*04fd306cSNickeau            $srcUrl = $fetchRaster->getFetchUrl()->toString();
2471fa8c418SNickeau            $srcSet .= "$srcUrl {$targetWidth}w";
2481fa8c418SNickeau            $sizes .= "{$targetWidth}px";
24937748cd8SNickeau        }
25037748cd8SNickeau
25137748cd8SNickeau        /**
25237748cd8SNickeau         * Lazy load
25337748cd8SNickeau         */
25437748cd8SNickeau        $lazyLoad = $this->getLazyLoad();
25537748cd8SNickeau        if ($lazyLoad) {
25637748cd8SNickeau
25737748cd8SNickeau            /**
2584cadd4f8SNickeau             * Html Lazy loading
2594cadd4f8SNickeau             */
260*04fd306cSNickeau            $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault();
2614cadd4f8SNickeau            switch ($lazyLoadMethod) {
262*04fd306cSNickeau                case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE:
263*04fd306cSNickeau                default:
2644cadd4f8SNickeau                    $attributes->addOutputAttributeValue("src", $srcValue);
2654cadd4f8SNickeau                    if (!empty($srcSet)) {
2664cadd4f8SNickeau                        // it the image is small, no srcset for instance
2674cadd4f8SNickeau                        $attributes->addOutputAttributeValue("srcset", $srcSet);
2684cadd4f8SNickeau                    }
2694cadd4f8SNickeau                    $attributes->addOutputAttributeValue("loading", "lazy");
2704cadd4f8SNickeau                    break;
271*04fd306cSNickeau                case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE:
2724cadd4f8SNickeau                    /**
27337748cd8SNickeau                     * Snippet Lazy loading
27437748cd8SNickeau                     */
27537748cd8SNickeau                    LazyLoad::addLozadSnippet();
276*04fd306cSNickeau                    PluginUtility::getSnippetManager()->attachJavascriptFromComponentId("lozad-raster");
277*04fd306cSNickeau                    $attributes->addClassName(self::getLazyClass());
278*04fd306cSNickeau                    $attributes->addClassName(LazyLoad::getLazyClass());
27937748cd8SNickeau
28037748cd8SNickeau                    /**
28137748cd8SNickeau                     * A small image has no srcset
28237748cd8SNickeau                     *
28337748cd8SNickeau                     */
28437748cd8SNickeau                    if (!empty($srcSet)) {
28537748cd8SNickeau
28637748cd8SNickeau                        /**
28737748cd8SNickeau                         * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!!
28837748cd8SNickeau                         * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern
28937748cd8SNickeau                         * The transparent image has a fix dimension aspect ratio of 1x1 making
29037748cd8SNickeau                         * a bad reserved space for the image
29137748cd8SNickeau                         * We use a svg instead
29237748cd8SNickeau                         */
2934cadd4f8SNickeau                        $attributes->addOutputAttributeValue("src", $srcValue);
2944cadd4f8SNickeau                        $attributes->addOutputAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight));
29537748cd8SNickeau                        /**
29637748cd8SNickeau                         * We use `data-sizes` and not `sizes`
29737748cd8SNickeau                         * because `sizes` without `srcset`
29837748cd8SNickeau                         * shows the broken image symbol
29937748cd8SNickeau                         * Javascript changes them at the same time
30037748cd8SNickeau                         */
3014cadd4f8SNickeau                        $attributes->addOutputAttributeValue("data-sizes", $sizes);
3024cadd4f8SNickeau                        $attributes->addOutputAttributeValue("data-srcset", $srcSet);
30337748cd8SNickeau
30437748cd8SNickeau                    } else {
30537748cd8SNickeau
30637748cd8SNickeau                        /**
30737748cd8SNickeau                         * Small image but there is no little improvement
30837748cd8SNickeau                         */
3094cadd4f8SNickeau                        $attributes->addOutputAttributeValue("data-src", $srcValue);
3104cadd4f8SNickeau                        $attributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight));
31137748cd8SNickeau
31237748cd8SNickeau                    }
3131fa8c418SNickeau                    LazyLoad::addPlaceholderBackground($attributes);
3144cadd4f8SNickeau                    break;
3154cadd4f8SNickeau            }
31637748cd8SNickeau
31737748cd8SNickeau
31837748cd8SNickeau        } else {
31937748cd8SNickeau
32037748cd8SNickeau            if (!empty($srcSet)) {
3214cadd4f8SNickeau                $attributes->addOutputAttributeValue("srcset", $srcSet);
3224cadd4f8SNickeau                $attributes->addOutputAttributeValue("sizes", $sizes);
32337748cd8SNickeau            } else {
3244cadd4f8SNickeau                $attributes->addOutputAttributeValue("src", $srcValue);
32537748cd8SNickeau            }
32637748cd8SNickeau
32737748cd8SNickeau        }
32837748cd8SNickeau
32937748cd8SNickeau
33037748cd8SNickeau        /**
33137748cd8SNickeau         * Title (ie alt)
33237748cd8SNickeau         */
333*04fd306cSNickeau        $attributes->addOutputAttributeValueIfNotEmpty("alt", $this->getAltNotEmpty());
334c3437056SNickeau
335c3437056SNickeau        /**
33637748cd8SNickeau         * Create the img element
33737748cd8SNickeau         */
3381fa8c418SNickeau        $htmlAttributes = $attributes->toHTMLAttributeString();
33937748cd8SNickeau        $imgHTML = '<img ' . $htmlAttributes . '/>';
34037748cd8SNickeau
34137748cd8SNickeau
342*04fd306cSNickeau        return $this->wrapMediaMarkupWithLink($imgHTML);
34337748cd8SNickeau    }
34437748cd8SNickeau
34537748cd8SNickeau
346*04fd306cSNickeau    public function getLazyLoad(): bool
34737748cd8SNickeau    {
348*04fd306cSNickeau
349*04fd306cSNickeau        if ($this->mediaMarkup->isLazy() === false) {
350*04fd306cSNickeau            return false;
35137748cd8SNickeau        }
352*04fd306cSNickeau        return SiteConfig::getConfValue(LazyLoad::CONF_RASTER_ENABLE, LazyLoad::CONF_RASTER_ENABLE_DEFAULT);
353*04fd306cSNickeau
35437748cd8SNickeau    }
35537748cd8SNickeau
35637748cd8SNickeau    /**
35737748cd8SNickeau     * @param $screenWidth
35837748cd8SNickeau     * @param $imageWidth
35937748cd8SNickeau     * @return string sizes with a dpi correction if
36037748cd8SNickeau     */
36137748cd8SNickeau    private
3621fa8c418SNickeau    function getSizes($screenWidth, $imageWidth): string
36337748cd8SNickeau    {
36437748cd8SNickeau
36537748cd8SNickeau        if ($this->getWithDpiCorrection()) {
36637748cd8SNickeau            $dpiBase = 96;
36737748cd8SNickeau            $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px";
36837748cd8SNickeau            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px";
36937748cd8SNickeau            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px";
37037748cd8SNickeau        } else {
37137748cd8SNickeau            $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px";
37237748cd8SNickeau        }
37337748cd8SNickeau        return $sizes;
37437748cd8SNickeau    }
37537748cd8SNickeau
37637748cd8SNickeau    /**
37737748cd8SNickeau     * Return if the DPI correction is enabled or not for responsive image
37837748cd8SNickeau     *
37937748cd8SNickeau     * Mobile have a higher DPI and can then fit a bigger image on a smaller size.
38037748cd8SNickeau     *
38137748cd8SNickeau     * This can be disturbing when debugging responsive sizing image
38237748cd8SNickeau     * If you want also to use less bandwidth, this is also useful.
38337748cd8SNickeau     *
38437748cd8SNickeau     * @return bool
38537748cd8SNickeau     */
38637748cd8SNickeau    private
3871fa8c418SNickeau    function getWithDpiCorrection(): bool
38837748cd8SNickeau    {
38937748cd8SNickeau        /**
39037748cd8SNickeau         * Support for retina means no DPI correction
39137748cd8SNickeau         */
392*04fd306cSNickeau        $retinaEnabled = SiteConfig::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0);
39337748cd8SNickeau        return !$retinaEnabled;
39437748cd8SNickeau    }
39537748cd8SNickeau
396*04fd306cSNickeau    /**
397*04fd306cSNickeau     * Used to select the raster image lazy loaded
398*04fd306cSNickeau     * @return string
399*04fd306cSNickeau     */
400*04fd306cSNickeau    public static function getLazyClass()
401*04fd306cSNickeau    {
402*04fd306cSNickeau        return StyleAttribute::addComboStrapSuffix("lazy-raster");
403*04fd306cSNickeau    }
404*04fd306cSNickeau
40537748cd8SNickeau
40637748cd8SNickeau}
407