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