xref: /plugin/combo/ComboStrap/RasterImageLink.php (revision 82a60d039cd81033dc8147c27f0a50716b7a5301)
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
1537748cd8SNickeaurequire_once(__DIR__ . '/MediaLink.php');
1637748cd8SNickeaurequire_once(__DIR__ . '/LazyLoad.php');
1737748cd8SNickeaurequire_once(__DIR__ . '/PluginUtility.php');
1837748cd8SNickeau
1937748cd8SNickeau/**
2037748cd8SNickeau * Image
2137748cd8SNickeau * This is the class that handles the
2237748cd8SNickeau * raster image type of the dokuwiki {@link MediaLink}
2337748cd8SNickeau *
2437748cd8SNickeau * The real documentation can be found on the image page
2537748cd8SNickeau * @link https://www.dokuwiki.org/images
2637748cd8SNickeau *
2737748cd8SNickeau * Doc:
2837748cd8SNickeau * https://web.dev/optimize-cls/#images-without-dimensions
2937748cd8SNickeau * https://web.dev/cls/
3037748cd8SNickeau */
311fa8c418SNickeauclass RasterImageLink extends ImageLink
3237748cd8SNickeau{
3337748cd8SNickeau
341fa8c418SNickeau    const CANONICAL = ImageRaster::CANONICAL;
3537748cd8SNickeau    const CONF_LAZY_LOADING_ENABLE = "rasterImageLazyLoadingEnable";
36c3437056SNickeau    const CONF_LAZY_LOADING_ENABLE_DEFAULT = 1;
3737748cd8SNickeau
3837748cd8SNickeau    const RESPONSIVE_CLASS = "img-fluid";
3937748cd8SNickeau
4037748cd8SNickeau    const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin";
4137748cd8SNickeau    const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable";
4237748cd8SNickeau    const LAZY_CLASS = "lazy-raster-combo";
4337748cd8SNickeau
4437748cd8SNickeau    const BREAKPOINTS =
4537748cd8SNickeau        array(
4637748cd8SNickeau            "xs" => 375,
4737748cd8SNickeau            "sm" => 576,
4837748cd8SNickeau            "md" => 768,
4937748cd8SNickeau            "lg" => 992
5037748cd8SNickeau        );
5137748cd8SNickeau
5237748cd8SNickeau
5337748cd8SNickeau    /**
5437748cd8SNickeau     * RasterImageLink constructor.
551fa8c418SNickeau     * @param ImageRaster $imageRaster
5637748cd8SNickeau     */
571fa8c418SNickeau    public function __construct($imageRaster)
5837748cd8SNickeau    {
591fa8c418SNickeau        parent::__construct($imageRaster);
6037748cd8SNickeau
6137748cd8SNickeau
6237748cd8SNickeau    }
6337748cd8SNickeau
6437748cd8SNickeau
6537748cd8SNickeau    /**
6637748cd8SNickeau     * Render a link
6737748cd8SNickeau     * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
6837748cd8SNickeau     * A media can be a video also (Use
6937748cd8SNickeau     * @return string
7037748cd8SNickeau     */
711fa8c418SNickeau    public function renderMediaTag(): string
7237748cd8SNickeau    {
73c3437056SNickeau        /**
74c3437056SNickeau         * @var ImageRaster $image
75c3437056SNickeau         */
761fa8c418SNickeau        $image = $this->getDefaultImage();
771fa8c418SNickeau        if ($image->exists()) {
7837748cd8SNickeau
791fa8c418SNickeau            $attributes = $image->getAttributes();
8037748cd8SNickeau
8137748cd8SNickeau            /**
8237748cd8SNickeau             * No dokuwiki type attribute
8337748cd8SNickeau             */
841fa8c418SNickeau            $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE);
851fa8c418SNickeau            $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC);
8637748cd8SNickeau
8737748cd8SNickeau            /**
8837748cd8SNickeau             * Responsive image
8937748cd8SNickeau             * https://getbootstrap.com/docs/5.0/content/images/
9037748cd8SNickeau             * to apply max-width: 100%; and height: auto;
9137748cd8SNickeau             *
9237748cd8SNickeau             * Even if the resizing is requested by height,
9337748cd8SNickeau             * the height: auto on styling is needed to conserve the ratio
9437748cd8SNickeau             * while scaling down the screen
9537748cd8SNickeau             */
961fa8c418SNickeau            $attributes->addClassName(self::RESPONSIVE_CLASS);
9737748cd8SNickeau
9837748cd8SNickeau
9937748cd8SNickeau            /**
10037748cd8SNickeau             * width and height to give the dimension ratio
10137748cd8SNickeau             * They have an effect on the space reservation
10237748cd8SNickeau             * but not on responsive image at all
10337748cd8SNickeau             * To allow responsive height, the height style property is set at auto
10437748cd8SNickeau             * (ie img-fluid in bootstrap)
10537748cd8SNickeau             */
10637748cd8SNickeau            // The unit is not mandatory in HTML, this is expected to be CSS pixel
10737748cd8SNickeau            // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
10837748cd8SNickeau            // The HTML validator does not expect an unit otherwise it send an error
10937748cd8SNickeau            // https://validator.w3.org/
11037748cd8SNickeau            $htmlLengthUnit = "";
111*82a60d03SNickeau            $cssLengthUnit = "px";
11237748cd8SNickeau
11337748cd8SNickeau            /**
11437748cd8SNickeau             * Height
11537748cd8SNickeau             * The logical height that the image should take on the page
11637748cd8SNickeau             *
11737748cd8SNickeau             * Note: The style is also set in {@link Dimension::processWidthAndHeight()}
11837748cd8SNickeau             *
11937748cd8SNickeau             */
120*82a60d03SNickeau            try {
1211fa8c418SNickeau                $targetHeight = $image->getTargetHeight();
122*82a60d03SNickeau            } catch (ExceptionCombo $e) {
123*82a60d03SNickeau                LogUtility::msg("No rendering for the image ($image). The target height reports a problem: {$e->getMessage()}");
124*82a60d03SNickeau                return "";
125*82a60d03SNickeau            }
1261fa8c418SNickeau            if (!empty($targetHeight)) {
127*82a60d03SNickeau                /**
128*82a60d03SNickeau                 * HTML height attribute is important for the ratio calculation
129*82a60d03SNickeau                 * No layout shift
130*82a60d03SNickeau                 */
1311fa8c418SNickeau                $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit);
132*82a60d03SNickeau                /**
133*82a60d03SNickeau                 * We don't allow the image to scale up by default
134*82a60d03SNickeau                 */
135*82a60d03SNickeau                $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit);
13637748cd8SNickeau            }
13737748cd8SNickeau
13837748cd8SNickeau
13937748cd8SNickeau            /**
14037748cd8SNickeau             * Width
14137748cd8SNickeau             *
14237748cd8SNickeau             * We create a series of URL
14337748cd8SNickeau             * for different width and let the browser
14437748cd8SNickeau             * download the best one for:
14537748cd8SNickeau             *   * the actual container width
14637748cd8SNickeau             *   * the actual of screen resolution
14737748cd8SNickeau             *   * and the connection speed.
14837748cd8SNickeau             *
14937748cd8SNickeau             * The max-width value is set
15037748cd8SNickeau             */
151*82a60d03SNickeau            try {
1521fa8c418SNickeau                $mediaWidthValue = $image->getIntrinsicWidth();
153*82a60d03SNickeau            } catch (ExceptionCombo $e) {
154*82a60d03SNickeau                LogUtility::msg("No rendering for the image ($image). The intrinsic width reports a problem: {$e->getMessage()}");
155*82a60d03SNickeau                return "";
156*82a60d03SNickeau            }
1571fa8c418SNickeau            $srcValue = $image->getUrl();
15837748cd8SNickeau
15937748cd8SNickeau            /**
16037748cd8SNickeau             * Responsive image src set building
16137748cd8SNickeau             * We have chosen
16237748cd8SNickeau             *   * 375: Iphone6
16337748cd8SNickeau             *   * 768: Ipad
16437748cd8SNickeau             *   * 1024: Ipad Pro
16537748cd8SNickeau             *
16637748cd8SNickeau             */
16737748cd8SNickeau            // The image margin applied
16837748cd8SNickeau            $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px");
16937748cd8SNickeau
17037748cd8SNickeau
17137748cd8SNickeau            /**
17237748cd8SNickeau             * Srcset and sizes for responsive image
17337748cd8SNickeau             * Width is mandatory for responsive image
17437748cd8SNickeau             * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images
17537748cd8SNickeau             */
17637748cd8SNickeau            if (!empty($mediaWidthValue)) {
17737748cd8SNickeau
17837748cd8SNickeau                /**
1791fa8c418SNickeau                 * The value of the target image
18037748cd8SNickeau                 */
181*82a60d03SNickeau                try {
1821fa8c418SNickeau                    $targetWidth = $image->getTargetWidth();
183*82a60d03SNickeau                } catch (ExceptionCombo $e) {
184*82a60d03SNickeau                    LogUtility::msg("No rendering for the image ($image). The target width reports a problem: {$e->getMessage()}");
185*82a60d03SNickeau                    return "";
186*82a60d03SNickeau                }
1871fa8c418SNickeau                if (!empty($targetWidth)) {
18837748cd8SNickeau
1891fa8c418SNickeau                    if (!empty($targetHeight)) {
1901fa8c418SNickeau                        $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight);
19137748cd8SNickeau                    }
192*82a60d03SNickeau                    /**
193*82a60d03SNickeau                     * HTML Width attribute is important to avoid layout shift
194*82a60d03SNickeau                     */
1951fa8c418SNickeau                    $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit);
196*82a60d03SNickeau                    /**
197*82a60d03SNickeau                     * We don't allow the image to scale up by default
198*82a60d03SNickeau                     */
199*82a60d03SNickeau                    $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit);
200*82a60d03SNickeau                    /**
201*82a60d03SNickeau                     * We allow the image to scale down up to 100% of its parent
202*82a60d03SNickeau                     */
203*82a60d03SNickeau                    $attributes->addStyleDeclarationIfNotSet("width", "100%");
204*82a60d03SNickeau
20537748cd8SNickeau                }
20637748cd8SNickeau
20737748cd8SNickeau                /**
20837748cd8SNickeau                 * Continue
20937748cd8SNickeau                 */
21037748cd8SNickeau                $srcSet = "";
21137748cd8SNickeau                $sizes = "";
21237748cd8SNickeau
21337748cd8SNickeau                /**
21437748cd8SNickeau                 * Add smaller sizes
21537748cd8SNickeau                 */
21637748cd8SNickeau                foreach (self::BREAKPOINTS as $breakpointWidth) {
21737748cd8SNickeau
2181fa8c418SNickeau                    if ($targetWidth > $breakpointWidth) {
21937748cd8SNickeau
22037748cd8SNickeau                        if (!empty($srcSet)) {
22137748cd8SNickeau                            $srcSet .= ", ";
22237748cd8SNickeau                            $sizes .= ", ";
22337748cd8SNickeau                        }
22437748cd8SNickeau                        $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin;
225c3437056SNickeau                        $xsmUrl = $image->getUrlForSrcSetAtBreakpoint($breakpointWidthMinusMargin);
22637748cd8SNickeau                        $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w";
22737748cd8SNickeau                        $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin);
22837748cd8SNickeau
22937748cd8SNickeau                    }
23037748cd8SNickeau
23137748cd8SNickeau                }
23237748cd8SNickeau
23337748cd8SNickeau                /**
23437748cd8SNickeau                 * Add the last size
2351fa8c418SNickeau                 * If the target image is really small, srcset and sizes are empty
23637748cd8SNickeau                 */
23737748cd8SNickeau                if (!empty($srcSet)) {
23837748cd8SNickeau                    $srcSet .= ", ";
23937748cd8SNickeau                    $sizes .= ", ";
240c3437056SNickeau                    $srcUrl = $image->getUrlForSrcSetAtBreakpoint($targetWidth);
2411fa8c418SNickeau                    $srcSet .= "$srcUrl {$targetWidth}w";
2421fa8c418SNickeau                    $sizes .= "{$targetWidth}px";
24337748cd8SNickeau                }
24437748cd8SNickeau
24537748cd8SNickeau                /**
24637748cd8SNickeau                 * Lazy load
24737748cd8SNickeau                 */
24837748cd8SNickeau                $lazyLoad = $this->getLazyLoad();
24937748cd8SNickeau                if ($lazyLoad) {
25037748cd8SNickeau
25137748cd8SNickeau                    /**
25237748cd8SNickeau                     * Snippet Lazy loading
25337748cd8SNickeau                     */
25437748cd8SNickeau                    LazyLoad::addLozadSnippet();
25537748cd8SNickeau                    PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster");
2561fa8c418SNickeau                    $attributes->addClassName(self::LAZY_CLASS);
2571fa8c418SNickeau                    $attributes->addClassName(LazyLoad::LAZY_CLASS);
25837748cd8SNickeau
25937748cd8SNickeau                    /**
26037748cd8SNickeau                     * A small image has no srcset
26137748cd8SNickeau                     *
26237748cd8SNickeau                     */
26337748cd8SNickeau                    if (!empty($srcSet)) {
26437748cd8SNickeau
26537748cd8SNickeau                        /**
26637748cd8SNickeau                         * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!!
26737748cd8SNickeau                         * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern
26837748cd8SNickeau                         * The transparent image has a fix dimension aspect ratio of 1x1 making
26937748cd8SNickeau                         * a bad reserved space for the image
27037748cd8SNickeau                         * We use a svg instead
27137748cd8SNickeau                         */
2721fa8c418SNickeau                        $attributes->addHtmlAttributeValue("src", $srcValue);
2731fa8c418SNickeau                        $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight));
27437748cd8SNickeau                        /**
27537748cd8SNickeau                         * We use `data-sizes` and not `sizes`
27637748cd8SNickeau                         * because `sizes` without `srcset`
27737748cd8SNickeau                         * shows the broken image symbol
27837748cd8SNickeau                         * Javascript changes them at the same time
27937748cd8SNickeau                         */
2801fa8c418SNickeau                        $attributes->addHtmlAttributeValue("data-sizes", $sizes);
2811fa8c418SNickeau                        $attributes->addHtmlAttributeValue("data-srcset", $srcSet);
28237748cd8SNickeau
28337748cd8SNickeau                    } else {
28437748cd8SNickeau
28537748cd8SNickeau                        /**
28637748cd8SNickeau                         * Small image but there is no little improvement
28737748cd8SNickeau                         */
2881fa8c418SNickeau                        $attributes->addHtmlAttributeValue("data-src", $srcValue);
289c3437056SNickeau                        $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight));
29037748cd8SNickeau
29137748cd8SNickeau                    }
29237748cd8SNickeau
2931fa8c418SNickeau                    LazyLoad::addPlaceholderBackground($attributes);
29437748cd8SNickeau
29537748cd8SNickeau
29637748cd8SNickeau                } else {
29737748cd8SNickeau
29837748cd8SNickeau                    if (!empty($srcSet)) {
2991fa8c418SNickeau                        $attributes->addHtmlAttributeValue("srcset", $srcSet);
3001fa8c418SNickeau                        $attributes->addHtmlAttributeValue("sizes", $sizes);
30137748cd8SNickeau                    } else {
3021fa8c418SNickeau                        $attributes->addHtmlAttributeValue("src", $srcValue);
30337748cd8SNickeau                    }
30437748cd8SNickeau
30537748cd8SNickeau                }
30637748cd8SNickeau
30737748cd8SNickeau            } else {
30837748cd8SNickeau
30937748cd8SNickeau                // No width, no responsive possibility
31037748cd8SNickeau                $lazyLoad = $this->getLazyLoad();
31137748cd8SNickeau                if ($lazyLoad) {
31237748cd8SNickeau
3131fa8c418SNickeau                    LazyLoad::addPlaceholderBackground($attributes);
3141fa8c418SNickeau                    $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder());
3151fa8c418SNickeau                    $attributes->addHtmlAttributeValue("data-src", $srcValue);
31637748cd8SNickeau
31737748cd8SNickeau                }
31837748cd8SNickeau
31937748cd8SNickeau            }
32037748cd8SNickeau
32137748cd8SNickeau
32237748cd8SNickeau            /**
32337748cd8SNickeau             * Title (ie alt)
32437748cd8SNickeau             */
3251fa8c418SNickeau            $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty());
3261fa8c418SNickeau
3271fa8c418SNickeau            /**
3281fa8c418SNickeau             * TODO: Side effect of the fact that we use the same attributes
3291fa8c418SNickeau             * Title attribute of a media is the alt of an image
3301fa8c418SNickeau             * And title should not be in an image tag
3311fa8c418SNickeau             */
3321fa8c418SNickeau            $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY);
33337748cd8SNickeau
33437748cd8SNickeau            /**
335c3437056SNickeau             * Old model where the src is parsed and the path
336c3437056SNickeau             * is in the attributes
337c3437056SNickeau             */
338c3437056SNickeau            $attributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME);
339c3437056SNickeau
340c3437056SNickeau            /**
34137748cd8SNickeau             * Create the img element
34237748cd8SNickeau             */
3431fa8c418SNickeau            $htmlAttributes = $attributes->toHTMLAttributeString();
34437748cd8SNickeau            $imgHTML = '<img ' . $htmlAttributes . '/>';
34537748cd8SNickeau
34637748cd8SNickeau        } else {
34737748cd8SNickeau
34837748cd8SNickeau            $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>";
34937748cd8SNickeau
35037748cd8SNickeau        }
35137748cd8SNickeau
35237748cd8SNickeau        return $imgHTML;
35337748cd8SNickeau    }
35437748cd8SNickeau
35537748cd8SNickeau
35637748cd8SNickeau    public
35737748cd8SNickeau    function getLazyLoad()
35837748cd8SNickeau    {
35937748cd8SNickeau        $lazyLoad = parent::getLazyLoad();
36037748cd8SNickeau        if ($lazyLoad !== null) {
36137748cd8SNickeau            return $lazyLoad;
36237748cd8SNickeau        } else {
363c3437056SNickeau            return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE, RasterImageLink::CONF_LAZY_LOADING_ENABLE_DEFAULT);
36437748cd8SNickeau        }
36537748cd8SNickeau    }
36637748cd8SNickeau
36737748cd8SNickeau    /**
36837748cd8SNickeau     * @param $screenWidth
36937748cd8SNickeau     * @param $imageWidth
37037748cd8SNickeau     * @return string sizes with a dpi correction if
37137748cd8SNickeau     */
37237748cd8SNickeau    private
3731fa8c418SNickeau    function getSizes($screenWidth, $imageWidth): string
37437748cd8SNickeau    {
37537748cd8SNickeau
37637748cd8SNickeau        if ($this->getWithDpiCorrection()) {
37737748cd8SNickeau            $dpiBase = 96;
37837748cd8SNickeau            $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px";
37937748cd8SNickeau            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px";
38037748cd8SNickeau            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px";
38137748cd8SNickeau        } else {
38237748cd8SNickeau            $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px";
38337748cd8SNickeau        }
38437748cd8SNickeau        return $sizes;
38537748cd8SNickeau    }
38637748cd8SNickeau
38737748cd8SNickeau    /**
38837748cd8SNickeau     * Return if the DPI correction is enabled or not for responsive image
38937748cd8SNickeau     *
39037748cd8SNickeau     * Mobile have a higher DPI and can then fit a bigger image on a smaller size.
39137748cd8SNickeau     *
39237748cd8SNickeau     * This can be disturbing when debugging responsive sizing image
39337748cd8SNickeau     * If you want also to use less bandwidth, this is also useful.
39437748cd8SNickeau     *
39537748cd8SNickeau     * @return bool
39637748cd8SNickeau     */
39737748cd8SNickeau    private
3981fa8c418SNickeau    function getWithDpiCorrection(): bool
39937748cd8SNickeau    {
40037748cd8SNickeau        /**
40137748cd8SNickeau         * Support for retina means no DPI correction
40237748cd8SNickeau         */
40337748cd8SNickeau        $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0);
40437748cd8SNickeau        return !$retinaEnabled;
40537748cd8SNickeau    }
40637748cd8SNickeau
40737748cd8SNickeau
40837748cd8SNickeau}
409