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 */ 31*1fa8c418SNickeauclass RasterImageLink extends ImageLink 3237748cd8SNickeau{ 3337748cd8SNickeau 34*1fa8c418SNickeau const CANONICAL = ImageRaster::CANONICAL; 3537748cd8SNickeau const CONF_LAZY_LOADING_ENABLE = "rasterImageLazyLoadingEnable"; 3637748cd8SNickeau 3737748cd8SNickeau const RESPONSIVE_CLASS = "img-fluid"; 3837748cd8SNickeau 3937748cd8SNickeau const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin"; 4037748cd8SNickeau const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable"; 4137748cd8SNickeau const LAZY_CLASS = "lazy-raster-combo"; 4237748cd8SNickeau 4337748cd8SNickeau const BREAKPOINTS = 4437748cd8SNickeau array( 4537748cd8SNickeau "xs" => 375, 4637748cd8SNickeau "sm" => 576, 4737748cd8SNickeau "md" => 768, 4837748cd8SNickeau "lg" => 992 4937748cd8SNickeau ); 5037748cd8SNickeau 5137748cd8SNickeau 5237748cd8SNickeau /** 5337748cd8SNickeau * RasterImageLink constructor. 54*1fa8c418SNickeau * @param ImageRaster $imageRaster 5537748cd8SNickeau * @param TagAttributes $tagAttributes 5637748cd8SNickeau */ 57*1fa8c418SNickeau public function __construct($imageRaster) 5837748cd8SNickeau { 59*1fa8c418SNickeau 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 */ 71*1fa8c418SNickeau public function renderMediaTag(): string 7237748cd8SNickeau { 7337748cd8SNickeau 74*1fa8c418SNickeau $image = $this->getDefaultImage(); 75*1fa8c418SNickeau if ($image->exists()) { 7637748cd8SNickeau 77*1fa8c418SNickeau $attributes = $image->getAttributes(); 7837748cd8SNickeau 7937748cd8SNickeau /** 8037748cd8SNickeau * No dokuwiki type attribute 8137748cd8SNickeau */ 82*1fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE); 83*1fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC); 8437748cd8SNickeau 8537748cd8SNickeau /** 8637748cd8SNickeau * Responsive image 8737748cd8SNickeau * https://getbootstrap.com/docs/5.0/content/images/ 8837748cd8SNickeau * to apply max-width: 100%; and height: auto; 8937748cd8SNickeau * 9037748cd8SNickeau * Even if the resizing is requested by height, 9137748cd8SNickeau * the height: auto on styling is needed to conserve the ratio 9237748cd8SNickeau * while scaling down the screen 9337748cd8SNickeau */ 94*1fa8c418SNickeau $attributes->addClassName(self::RESPONSIVE_CLASS); 9537748cd8SNickeau 9637748cd8SNickeau 9737748cd8SNickeau /** 9837748cd8SNickeau * width and height to give the dimension ratio 9937748cd8SNickeau * They have an effect on the space reservation 10037748cd8SNickeau * but not on responsive image at all 10137748cd8SNickeau * To allow responsive height, the height style property is set at auto 10237748cd8SNickeau * (ie img-fluid in bootstrap) 10337748cd8SNickeau */ 10437748cd8SNickeau // The unit is not mandatory in HTML, this is expected to be CSS pixel 10537748cd8SNickeau // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 10637748cd8SNickeau // The HTML validator does not expect an unit otherwise it send an error 10737748cd8SNickeau // https://validator.w3.org/ 10837748cd8SNickeau $htmlLengthUnit = ""; 10937748cd8SNickeau 11037748cd8SNickeau /** 11137748cd8SNickeau * Height 11237748cd8SNickeau * The logical height that the image should take on the page 11337748cd8SNickeau * 11437748cd8SNickeau * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 11537748cd8SNickeau * 11637748cd8SNickeau */ 117*1fa8c418SNickeau $targetHeight = $image->getTargetHeight(); 118*1fa8c418SNickeau if (!empty($targetHeight)) { 119*1fa8c418SNickeau $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit); 12037748cd8SNickeau } 12137748cd8SNickeau 12237748cd8SNickeau 12337748cd8SNickeau /** 12437748cd8SNickeau * Width 12537748cd8SNickeau * 12637748cd8SNickeau * We create a series of URL 12737748cd8SNickeau * for different width and let the browser 12837748cd8SNickeau * download the best one for: 12937748cd8SNickeau * * the actual container width 13037748cd8SNickeau * * the actual of screen resolution 13137748cd8SNickeau * * and the connection speed. 13237748cd8SNickeau * 13337748cd8SNickeau * The max-width value is set 13437748cd8SNickeau */ 135*1fa8c418SNickeau $mediaWidthValue = $image->getIntrinsicWidth(); 136*1fa8c418SNickeau $srcValue = $image->getUrl(); 13737748cd8SNickeau 13837748cd8SNickeau /** 13937748cd8SNickeau * Responsive image src set building 14037748cd8SNickeau * We have chosen 14137748cd8SNickeau * * 375: Iphone6 14237748cd8SNickeau * * 768: Ipad 14337748cd8SNickeau * * 1024: Ipad Pro 14437748cd8SNickeau * 14537748cd8SNickeau */ 14637748cd8SNickeau // The image margin applied 14737748cd8SNickeau $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 14837748cd8SNickeau 14937748cd8SNickeau 15037748cd8SNickeau /** 15137748cd8SNickeau * Srcset and sizes for responsive image 15237748cd8SNickeau * Width is mandatory for responsive image 15337748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 15437748cd8SNickeau */ 15537748cd8SNickeau if (!empty($mediaWidthValue)) { 15637748cd8SNickeau 15737748cd8SNickeau /** 158*1fa8c418SNickeau * The value of the target image 15937748cd8SNickeau */ 160*1fa8c418SNickeau $targetWidth = $image->getTargetWidth(); 161*1fa8c418SNickeau if (!empty($targetWidth)) { 16237748cd8SNickeau 163*1fa8c418SNickeau if (!empty($targetHeight)) { 164*1fa8c418SNickeau $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 16537748cd8SNickeau } 166*1fa8c418SNickeau $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit); 16737748cd8SNickeau } 16837748cd8SNickeau 16937748cd8SNickeau /** 17037748cd8SNickeau * Continue 17137748cd8SNickeau */ 17237748cd8SNickeau $srcSet = ""; 17337748cd8SNickeau $sizes = ""; 17437748cd8SNickeau 17537748cd8SNickeau /** 17637748cd8SNickeau * Add smaller sizes 17737748cd8SNickeau */ 17837748cd8SNickeau foreach (self::BREAKPOINTS as $breakpointWidth) { 17937748cd8SNickeau 180*1fa8c418SNickeau if ($targetWidth > $breakpointWidth) { 18137748cd8SNickeau 18237748cd8SNickeau if (!empty($srcSet)) { 18337748cd8SNickeau $srcSet .= ", "; 18437748cd8SNickeau $sizes .= ", "; 18537748cd8SNickeau } 18637748cd8SNickeau $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin; 187*1fa8c418SNickeau $xsmUrl = $image->getUrl(DokuwikiUrl::URL_ENCODED_AND, $breakpointWidthMinusMargin); 18837748cd8SNickeau $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w"; 18937748cd8SNickeau $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin); 19037748cd8SNickeau 19137748cd8SNickeau } 19237748cd8SNickeau 19337748cd8SNickeau } 19437748cd8SNickeau 19537748cd8SNickeau /** 19637748cd8SNickeau * Add the last size 197*1fa8c418SNickeau * If the target image is really small, srcset and sizes are empty 19837748cd8SNickeau */ 19937748cd8SNickeau if (!empty($srcSet)) { 20037748cd8SNickeau $srcSet .= ", "; 20137748cd8SNickeau $sizes .= ", "; 202*1fa8c418SNickeau $srcUrl = $image->getUrl(DokuwikiUrl::URL_ENCODED_AND, $targetWidth); 203*1fa8c418SNickeau $srcSet .= "$srcUrl {$targetWidth}w"; 204*1fa8c418SNickeau $sizes .= "{$targetWidth}px"; 20537748cd8SNickeau } 20637748cd8SNickeau 20737748cd8SNickeau /** 20837748cd8SNickeau * Lazy load 20937748cd8SNickeau */ 21037748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 21137748cd8SNickeau if ($lazyLoad) { 21237748cd8SNickeau 21337748cd8SNickeau /** 21437748cd8SNickeau * Snippet Lazy loading 21537748cd8SNickeau */ 21637748cd8SNickeau LazyLoad::addLozadSnippet(); 21737748cd8SNickeau PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster"); 218*1fa8c418SNickeau $attributes->addClassName(self::LAZY_CLASS); 219*1fa8c418SNickeau $attributes->addClassName(LazyLoad::LAZY_CLASS); 22037748cd8SNickeau 22137748cd8SNickeau /** 22237748cd8SNickeau * A small image has no srcset 22337748cd8SNickeau * 22437748cd8SNickeau */ 22537748cd8SNickeau if (!empty($srcSet)) { 22637748cd8SNickeau 22737748cd8SNickeau /** 22837748cd8SNickeau * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 22937748cd8SNickeau * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 23037748cd8SNickeau * The transparent image has a fix dimension aspect ratio of 1x1 making 23137748cd8SNickeau * a bad reserved space for the image 23237748cd8SNickeau * We use a svg instead 23337748cd8SNickeau */ 234*1fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 235*1fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 23637748cd8SNickeau /** 23737748cd8SNickeau * We use `data-sizes` and not `sizes` 23837748cd8SNickeau * because `sizes` without `srcset` 23937748cd8SNickeau * shows the broken image symbol 24037748cd8SNickeau * Javascript changes them at the same time 24137748cd8SNickeau */ 242*1fa8c418SNickeau $attributes->addHtmlAttributeValue("data-sizes", $sizes); 243*1fa8c418SNickeau $attributes->addHtmlAttributeValue("data-srcset", $srcSet); 24437748cd8SNickeau 24537748cd8SNickeau } else { 24637748cd8SNickeau 24737748cd8SNickeau /** 24837748cd8SNickeau * Small image but there is no little improvement 24937748cd8SNickeau */ 250*1fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 25137748cd8SNickeau 25237748cd8SNickeau } 25337748cd8SNickeau 254*1fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 25537748cd8SNickeau 25637748cd8SNickeau 25737748cd8SNickeau } else { 25837748cd8SNickeau 25937748cd8SNickeau if (!empty($srcSet)) { 260*1fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", $srcSet); 261*1fa8c418SNickeau $attributes->addHtmlAttributeValue("sizes", $sizes); 26237748cd8SNickeau } else { 263*1fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 26437748cd8SNickeau } 26537748cd8SNickeau 26637748cd8SNickeau } 26737748cd8SNickeau 26837748cd8SNickeau } else { 26937748cd8SNickeau 27037748cd8SNickeau // No width, no responsive possibility 27137748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 27237748cd8SNickeau if ($lazyLoad) { 27337748cd8SNickeau 274*1fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 275*1fa8c418SNickeau $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder()); 276*1fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 27737748cd8SNickeau 27837748cd8SNickeau } 27937748cd8SNickeau 28037748cd8SNickeau } 28137748cd8SNickeau 28237748cd8SNickeau 28337748cd8SNickeau /** 28437748cd8SNickeau * Title (ie alt) 28537748cd8SNickeau */ 286*1fa8c418SNickeau $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty()); 287*1fa8c418SNickeau 288*1fa8c418SNickeau /** 289*1fa8c418SNickeau * TODO: Side effect of the fact that we use the same attributes 290*1fa8c418SNickeau * Title attribute of a media is the alt of an image 291*1fa8c418SNickeau * And title should not be in an image tag 292*1fa8c418SNickeau */ 293*1fa8c418SNickeau $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY); 29437748cd8SNickeau 29537748cd8SNickeau /** 29637748cd8SNickeau * Create the img element 29737748cd8SNickeau */ 298*1fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 29937748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 30037748cd8SNickeau 30137748cd8SNickeau } else { 30237748cd8SNickeau 30337748cd8SNickeau $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>"; 30437748cd8SNickeau 30537748cd8SNickeau } 30637748cd8SNickeau 30737748cd8SNickeau return $imgHTML; 30837748cd8SNickeau } 30937748cd8SNickeau 31037748cd8SNickeau 31137748cd8SNickeau public 31237748cd8SNickeau function getLazyLoad() 31337748cd8SNickeau { 31437748cd8SNickeau $lazyLoad = parent::getLazyLoad(); 31537748cd8SNickeau if ($lazyLoad !== null) { 31637748cd8SNickeau return $lazyLoad; 31737748cd8SNickeau } else { 31837748cd8SNickeau return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE); 31937748cd8SNickeau } 32037748cd8SNickeau } 32137748cd8SNickeau 32237748cd8SNickeau /** 32337748cd8SNickeau * @param $screenWidth 32437748cd8SNickeau * @param $imageWidth 32537748cd8SNickeau * @return string sizes with a dpi correction if 32637748cd8SNickeau */ 32737748cd8SNickeau private 328*1fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 32937748cd8SNickeau { 33037748cd8SNickeau 33137748cd8SNickeau if ($this->getWithDpiCorrection()) { 33237748cd8SNickeau $dpiBase = 96; 33337748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 33437748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 33537748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 33637748cd8SNickeau } else { 33737748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 33837748cd8SNickeau } 33937748cd8SNickeau return $sizes; 34037748cd8SNickeau } 34137748cd8SNickeau 34237748cd8SNickeau /** 34337748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 34437748cd8SNickeau * 34537748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 34637748cd8SNickeau * 34737748cd8SNickeau * This can be disturbing when debugging responsive sizing image 34837748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 34937748cd8SNickeau * 35037748cd8SNickeau * @return bool 35137748cd8SNickeau */ 35237748cd8SNickeau private 353*1fa8c418SNickeau function getWithDpiCorrection(): bool 35437748cd8SNickeau { 35537748cd8SNickeau /** 35637748cd8SNickeau * Support for retina means no DPI correction 35737748cd8SNickeau */ 35837748cd8SNickeau $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 35937748cd8SNickeau return !$retinaEnabled; 36037748cd8SNickeau } 36137748cd8SNickeau 36237748cd8SNickeau 36337748cd8SNickeau} 364