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 const CONF_LAZY_LOADING_ENABLE_DEFAULT = 1; 37 38 const RESPONSIVE_CLASS = "img-fluid"; 39 40 const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin"; 41 const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable"; 42 const LAZY_CLASS = "lazy-raster-combo"; 43 44 const BREAKPOINTS = 45 array( 46 "xs" => 375, 47 "sm" => 576, 48 "md" => 768, 49 "lg" => 992 50 ); 51 52 53 /** 54 * RasterImageLink constructor. 55 * @param ImageRaster $imageRaster 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 * @var ImageRaster $image 75 */ 76 $image = $this->getDefaultImage(); 77 if ($image->exists()) { 78 79 $attributes = $image->getAttributes(); 80 81 /** 82 * No dokuwiki type attribute 83 */ 84 $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE); 85 $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC); 86 87 /** 88 * Responsive image 89 * https://getbootstrap.com/docs/5.0/content/images/ 90 * to apply max-width: 100%; and height: auto; 91 * 92 * Even if the resizing is requested by height, 93 * the height: auto on styling is needed to conserve the ratio 94 * while scaling down the screen 95 */ 96 $attributes->addClassName(self::RESPONSIVE_CLASS); 97 98 99 /** 100 * width and height to give the dimension ratio 101 * They have an effect on the space reservation 102 * but not on responsive image at all 103 * To allow responsive height, the height style property is set at auto 104 * (ie img-fluid in bootstrap) 105 */ 106 // The unit is not mandatory in HTML, this is expected to be CSS pixel 107 // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 108 // The HTML validator does not expect an unit otherwise it send an error 109 // https://validator.w3.org/ 110 $htmlLengthUnit = ""; 111 $cssLengthUnit = "px"; 112 113 /** 114 * Height 115 * The logical height that the image should take on the page 116 * 117 * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 118 * 119 */ 120 try { 121 $targetHeight = $image->getTargetHeight(); 122 } catch (ExceptionCombo $e) { 123 LogUtility::msg("No rendering for the image ($image). The target height reports a problem: {$e->getMessage()}"); 124 return ""; 125 } 126 if (!empty($targetHeight)) { 127 /** 128 * HTML height attribute is important for the ratio calculation 129 * No layout shift 130 */ 131 $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit); 132 /** 133 * We don't allow the image to scale up by default 134 */ 135 $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit); 136 } 137 138 139 /** 140 * Width 141 * 142 * We create a series of URL 143 * for different width and let the browser 144 * download the best one for: 145 * * the actual container width 146 * * the actual of screen resolution 147 * * and the connection speed. 148 * 149 * The max-width value is set 150 */ 151 try { 152 $mediaWidthValue = $image->getIntrinsicWidth(); 153 } catch (ExceptionCombo $e) { 154 LogUtility::msg("No rendering for the image ($image). The intrinsic width reports a problem: {$e->getMessage()}"); 155 return ""; 156 } 157 $srcValue = $image->getUrl(); 158 159 /** 160 * Responsive image src set building 161 * We have chosen 162 * * 375: Iphone6 163 * * 768: Ipad 164 * * 1024: Ipad Pro 165 * 166 */ 167 // The image margin applied 168 $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 169 170 171 /** 172 * Srcset and sizes for responsive image 173 * Width is mandatory for responsive image 174 * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 175 */ 176 if (!empty($mediaWidthValue)) { 177 178 /** 179 * The value of the target image 180 */ 181 try { 182 $targetWidth = $image->getTargetWidth(); 183 } catch (ExceptionCombo $e) { 184 LogUtility::msg("No rendering for the image ($image). The target width reports a problem: {$e->getMessage()}"); 185 return ""; 186 } 187 if (!empty($targetWidth)) { 188 189 if (!empty($targetHeight)) { 190 $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 191 } 192 /** 193 * HTML Width attribute is important to avoid layout shift 194 */ 195 $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit); 196 /** 197 * We don't allow the image to scale up by default 198 */ 199 $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit); 200 /** 201 * We allow the image to scale down up to 100% of its parent 202 */ 203 $attributes->addStyleDeclarationIfNotSet("width", "100%"); 204 205 } 206 207 /** 208 * Continue 209 */ 210 $srcSet = ""; 211 $sizes = ""; 212 213 /** 214 * Add smaller sizes 215 */ 216 foreach (self::BREAKPOINTS as $breakpointWidth) { 217 218 if ($targetWidth > $breakpointWidth) { 219 220 if (!empty($srcSet)) { 221 $srcSet .= ", "; 222 $sizes .= ", "; 223 } 224 $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin; 225 $xsmUrl = $image->getUrlForSrcSetAtBreakpoint($breakpointWidthMinusMargin); 226 $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w"; 227 $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin); 228 229 } 230 231 } 232 233 /** 234 * Add the last size 235 * If the target image is really small, srcset and sizes are empty 236 */ 237 if (!empty($srcSet)) { 238 $srcSet .= ", "; 239 $sizes .= ", "; 240 $srcUrl = $image->getUrlForSrcSetAtBreakpoint($targetWidth); 241 $srcSet .= "$srcUrl {$targetWidth}w"; 242 $sizes .= "{$targetWidth}px"; 243 } 244 245 /** 246 * Lazy load 247 */ 248 $lazyLoad = $this->getLazyLoad(); 249 if ($lazyLoad) { 250 251 /** 252 * Snippet Lazy loading 253 */ 254 LazyLoad::addLozadSnippet(); 255 PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster"); 256 $attributes->addClassName(self::LAZY_CLASS); 257 $attributes->addClassName(LazyLoad::LAZY_CLASS); 258 259 /** 260 * A small image has no srcset 261 * 262 */ 263 if (!empty($srcSet)) { 264 265 /** 266 * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 267 * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 268 * The transparent image has a fix dimension aspect ratio of 1x1 making 269 * a bad reserved space for the image 270 * We use a svg instead 271 */ 272 $attributes->addHtmlAttributeValue("src", $srcValue); 273 $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 274 /** 275 * We use `data-sizes` and not `sizes` 276 * because `sizes` without `srcset` 277 * shows the broken image symbol 278 * Javascript changes them at the same time 279 */ 280 $attributes->addHtmlAttributeValue("data-sizes", $sizes); 281 $attributes->addHtmlAttributeValue("data-srcset", $srcSet); 282 283 } else { 284 285 /** 286 * Small image but there is no little improvement 287 */ 288 $attributes->addHtmlAttributeValue("data-src", $srcValue); 289 $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 290 291 } 292 293 LazyLoad::addPlaceholderBackground($attributes); 294 295 296 } else { 297 298 if (!empty($srcSet)) { 299 $attributes->addHtmlAttributeValue("srcset", $srcSet); 300 $attributes->addHtmlAttributeValue("sizes", $sizes); 301 } else { 302 $attributes->addHtmlAttributeValue("src", $srcValue); 303 } 304 305 } 306 307 } else { 308 309 // No width, no responsive possibility 310 $lazyLoad = $this->getLazyLoad(); 311 if ($lazyLoad) { 312 313 LazyLoad::addPlaceholderBackground($attributes); 314 $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder()); 315 $attributes->addHtmlAttributeValue("data-src", $srcValue); 316 317 } 318 319 } 320 321 322 /** 323 * Title (ie alt) 324 */ 325 $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty()); 326 327 /** 328 * TODO: Side effect of the fact that we use the same attributes 329 * Title attribute of a media is the alt of an image 330 * And title should not be in an image tag 331 */ 332 $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY); 333 334 /** 335 * Old model where the src is parsed and the path 336 * is in the attributes 337 */ 338 $attributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME); 339 340 /** 341 * Create the img element 342 */ 343 $htmlAttributes = $attributes->toHTMLAttributeString(); 344 $imgHTML = '<img ' . $htmlAttributes . '/>'; 345 346 } else { 347 348 $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>"; 349 350 } 351 352 return $imgHTML; 353 } 354 355 356 public 357 function getLazyLoad() 358 { 359 $lazyLoad = parent::getLazyLoad(); 360 if ($lazyLoad !== null) { 361 return $lazyLoad; 362 } else { 363 return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE, RasterImageLink::CONF_LAZY_LOADING_ENABLE_DEFAULT); 364 } 365 } 366 367 /** 368 * @param $screenWidth 369 * @param $imageWidth 370 * @return string sizes with a dpi correction if 371 */ 372 private 373 function getSizes($screenWidth, $imageWidth): string 374 { 375 376 if ($this->getWithDpiCorrection()) { 377 $dpiBase = 96; 378 $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 379 $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 380 $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 381 } else { 382 $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 383 } 384 return $sizes; 385 } 386 387 /** 388 * Return if the DPI correction is enabled or not for responsive image 389 * 390 * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 391 * 392 * This can be disturbing when debugging responsive sizing image 393 * If you want also to use less bandwidth, this is also useful. 394 * 395 * @return bool 396 */ 397 private 398 function getWithDpiCorrection(): bool 399 { 400 /** 401 * Support for retina means no DPI correction 402 */ 403 $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 404 return !$retinaEnabled; 405 } 406 407 408} 409