xref: /plugin/combo/ComboStrap/RasterImageLink.php (revision 1fa8c418ed5809db58049141be41b7738471dd32)
1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15require_once(__DIR__ . '/MediaLink.php');
16require_once(__DIR__ . '/LazyLoad.php');
17require_once(__DIR__ . '/PluginUtility.php');
18
19/**
20 * Image
21 * This is the class that handles the
22 * raster image type of the dokuwiki {@link MediaLink}
23 *
24 * The real documentation can be found on the image page
25 * @link https://www.dokuwiki.org/images
26 *
27 * Doc:
28 * https://web.dev/optimize-cls/#images-without-dimensions
29 * https://web.dev/cls/
30 */
31class RasterImageLink extends ImageLink
32{
33
34    const CANONICAL = ImageRaster::CANONICAL;
35    const CONF_LAZY_LOADING_ENABLE = "rasterImageLazyLoadingEnable";
36
37    const RESPONSIVE_CLASS = "img-fluid";
38
39    const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin";
40    const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable";
41    const LAZY_CLASS = "lazy-raster-combo";
42
43    const BREAKPOINTS =
44        array(
45            "xs" => 375,
46            "sm" => 576,
47            "md" => 768,
48            "lg" => 992
49        );
50
51
52    /**
53     * RasterImageLink constructor.
54     * @param ImageRaster $imageRaster
55     * @param TagAttributes $tagAttributes
56     */
57    public function __construct($imageRaster)
58    {
59        parent::__construct($imageRaster);
60
61
62    }
63
64
65    /**
66     * Render a link
67     * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
68     * A media can be a video also (Use
69     * @return string
70     */
71    public function renderMediaTag(): string
72    {
73
74        $image = $this->getDefaultImage();
75        if ($image->exists()) {
76
77            $attributes = $image->getAttributes();
78
79            /**
80             * No dokuwiki type attribute
81             */
82            $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE);
83            $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC);
84
85            /**
86             * Responsive image
87             * https://getbootstrap.com/docs/5.0/content/images/
88             * to apply max-width: 100%; and height: auto;
89             *
90             * Even if the resizing is requested by height,
91             * the height: auto on styling is needed to conserve the ratio
92             * while scaling down the screen
93             */
94            $attributes->addClassName(self::RESPONSIVE_CLASS);
95
96
97            /**
98             * width and height to give the dimension ratio
99             * They have an effect on the space reservation
100             * but not on responsive image at all
101             * To allow responsive height, the height style property is set at auto
102             * (ie img-fluid in bootstrap)
103             */
104            // The unit is not mandatory in HTML, this is expected to be CSS pixel
105            // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
106            // The HTML validator does not expect an unit otherwise it send an error
107            // https://validator.w3.org/
108            $htmlLengthUnit = "";
109
110            /**
111             * Height
112             * The logical height that the image should take on the page
113             *
114             * Note: The style is also set in {@link Dimension::processWidthAndHeight()}
115             *
116             */
117            $targetHeight = $image->getTargetHeight();
118            if (!empty($targetHeight)) {
119                $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit);
120            }
121
122
123            /**
124             * Width
125             *
126             * We create a series of URL
127             * for different width and let the browser
128             * download the best one for:
129             *   * the actual container width
130             *   * the actual of screen resolution
131             *   * and the connection speed.
132             *
133             * The max-width value is set
134             */
135            $mediaWidthValue = $image->getIntrinsicWidth();
136            $srcValue = $image->getUrl();
137
138            /**
139             * Responsive image src set building
140             * We have chosen
141             *   * 375: Iphone6
142             *   * 768: Ipad
143             *   * 1024: Ipad Pro
144             *
145             */
146            // The image margin applied
147            $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px");
148
149
150            /**
151             * Srcset and sizes for responsive image
152             * Width is mandatory for responsive image
153             * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images
154             */
155            if (!empty($mediaWidthValue)) {
156
157                /**
158                 * The value of the target image
159                 */
160                $targetWidth = $image->getTargetWidth();
161                if (!empty($targetWidth)) {
162
163                    if (!empty($targetHeight)) {
164                        $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight);
165                    }
166                    $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit);
167                }
168
169                /**
170                 * Continue
171                 */
172                $srcSet = "";
173                $sizes = "";
174
175                /**
176                 * Add smaller sizes
177                 */
178                foreach (self::BREAKPOINTS as $breakpointWidth) {
179
180                    if ($targetWidth > $breakpointWidth) {
181
182                        if (!empty($srcSet)) {
183                            $srcSet .= ", ";
184                            $sizes .= ", ";
185                        }
186                        $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin;
187                        $xsmUrl = $image->getUrl(DokuwikiUrl::URL_ENCODED_AND, $breakpointWidthMinusMargin);
188                        $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w";
189                        $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin);
190
191                    }
192
193                }
194
195                /**
196                 * Add the last size
197                 * If the target image is really small, srcset and sizes are empty
198                 */
199                if (!empty($srcSet)) {
200                    $srcSet .= ", ";
201                    $sizes .= ", ";
202                    $srcUrl = $image->getUrl(DokuwikiUrl::URL_ENCODED_AND, $targetWidth);
203                    $srcSet .= "$srcUrl {$targetWidth}w";
204                    $sizes .= "{$targetWidth}px";
205                }
206
207                /**
208                 * Lazy load
209                 */
210                $lazyLoad = $this->getLazyLoad();
211                if ($lazyLoad) {
212
213                    /**
214                     * Snippet Lazy loading
215                     */
216                    LazyLoad::addLozadSnippet();
217                    PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster");
218                    $attributes->addClassName(self::LAZY_CLASS);
219                    $attributes->addClassName(LazyLoad::LAZY_CLASS);
220
221                    /**
222                     * A small image has no srcset
223                     *
224                     */
225                    if (!empty($srcSet)) {
226
227                        /**
228                         * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!!
229                         * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern
230                         * The transparent image has a fix dimension aspect ratio of 1x1 making
231                         * a bad reserved space for the image
232                         * We use a svg instead
233                         */
234                        $attributes->addHtmlAttributeValue("src", $srcValue);
235                        $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight));
236                        /**
237                         * We use `data-sizes` and not `sizes`
238                         * because `sizes` without `srcset`
239                         * shows the broken image symbol
240                         * Javascript changes them at the same time
241                         */
242                        $attributes->addHtmlAttributeValue("data-sizes", $sizes);
243                        $attributes->addHtmlAttributeValue("data-srcset", $srcSet);
244
245                    } else {
246
247                        /**
248                         * Small image but there is no little improvement
249                         */
250                        $attributes->addHtmlAttributeValue("data-src", $srcValue);
251
252                    }
253
254                    LazyLoad::addPlaceholderBackground($attributes);
255
256
257                } else {
258
259                    if (!empty($srcSet)) {
260                        $attributes->addHtmlAttributeValue("srcset", $srcSet);
261                        $attributes->addHtmlAttributeValue("sizes", $sizes);
262                    } else {
263                        $attributes->addHtmlAttributeValue("src", $srcValue);
264                    }
265
266                }
267
268            } else {
269
270                // No width, no responsive possibility
271                $lazyLoad = $this->getLazyLoad();
272                if ($lazyLoad) {
273
274                    LazyLoad::addPlaceholderBackground($attributes);
275                    $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder());
276                    $attributes->addHtmlAttributeValue("data-src", $srcValue);
277
278                }
279
280            }
281
282
283            /**
284             * Title (ie alt)
285             */
286            $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty());
287
288            /**
289             * TODO: Side effect of the fact that we use the same attributes
290             * Title attribute of a media is the alt of an image
291             * And title should not be in an image tag
292             */
293            $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY);
294
295            /**
296             * Create the img element
297             */
298            $htmlAttributes = $attributes->toHTMLAttributeString();
299            $imgHTML = '<img ' . $htmlAttributes . '/>';
300
301        } else {
302
303            $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>";
304
305        }
306
307        return $imgHTML;
308    }
309
310
311    public
312    function getLazyLoad()
313    {
314        $lazyLoad = parent::getLazyLoad();
315        if ($lazyLoad !== null) {
316            return $lazyLoad;
317        } else {
318            return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE);
319        }
320    }
321
322    /**
323     * @param $screenWidth
324     * @param $imageWidth
325     * @return string sizes with a dpi correction if
326     */
327    private
328    function getSizes($screenWidth, $imageWidth): string
329    {
330
331        if ($this->getWithDpiCorrection()) {
332            $dpiBase = 96;
333            $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px";
334            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px";
335            $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px";
336        } else {
337            $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px";
338        }
339        return $sizes;
340    }
341
342    /**
343     * Return if the DPI correction is enabled or not for responsive image
344     *
345     * Mobile have a higher DPI and can then fit a bigger image on a smaller size.
346     *
347     * This can be disturbing when debugging responsive sizing image
348     * If you want also to use less bandwidth, this is also useful.
349     *
350     * @return bool
351     */
352    private
353    function getWithDpiCorrection(): bool
354    {
355        /**
356         * Support for retina means no DPI correction
357         */
358        $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0);
359        return !$retinaEnabled;
360    }
361
362
363}
364