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