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 */ 311fa8c418SNickeauclass RasterImageLink extends ImageLink 3237748cd8SNickeau{ 3337748cd8SNickeau 341fa8c418SNickeau const CANONICAL = ImageRaster::CANONICAL; 3537748cd8SNickeau const CONF_LAZY_LOADING_ENABLE = "rasterImageLazyLoadingEnable"; 36*c3437056SNickeau const CONF_LAZY_LOADING_ENABLE_DEFAULT = 1; 3737748cd8SNickeau 3837748cd8SNickeau const RESPONSIVE_CLASS = "img-fluid"; 3937748cd8SNickeau 4037748cd8SNickeau const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin"; 4137748cd8SNickeau const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable"; 4237748cd8SNickeau const LAZY_CLASS = "lazy-raster-combo"; 4337748cd8SNickeau 4437748cd8SNickeau const BREAKPOINTS = 4537748cd8SNickeau array( 4637748cd8SNickeau "xs" => 375, 4737748cd8SNickeau "sm" => 576, 4837748cd8SNickeau "md" => 768, 4937748cd8SNickeau "lg" => 992 5037748cd8SNickeau ); 5137748cd8SNickeau 5237748cd8SNickeau 5337748cd8SNickeau /** 5437748cd8SNickeau * RasterImageLink constructor. 551fa8c418SNickeau * @param ImageRaster $imageRaster 5637748cd8SNickeau */ 571fa8c418SNickeau public function __construct($imageRaster) 5837748cd8SNickeau { 591fa8c418SNickeau 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 */ 711fa8c418SNickeau public function renderMediaTag(): string 7237748cd8SNickeau { 73*c3437056SNickeau /** 74*c3437056SNickeau * @var ImageRaster $image 75*c3437056SNickeau */ 761fa8c418SNickeau $image = $this->getDefaultImage(); 771fa8c418SNickeau if ($image->exists()) { 7837748cd8SNickeau 791fa8c418SNickeau $attributes = $image->getAttributes(); 8037748cd8SNickeau 8137748cd8SNickeau /** 8237748cd8SNickeau * No dokuwiki type attribute 8337748cd8SNickeau */ 841fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE); 851fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC); 8637748cd8SNickeau 8737748cd8SNickeau /** 8837748cd8SNickeau * Responsive image 8937748cd8SNickeau * https://getbootstrap.com/docs/5.0/content/images/ 9037748cd8SNickeau * to apply max-width: 100%; and height: auto; 9137748cd8SNickeau * 9237748cd8SNickeau * Even if the resizing is requested by height, 9337748cd8SNickeau * the height: auto on styling is needed to conserve the ratio 9437748cd8SNickeau * while scaling down the screen 9537748cd8SNickeau */ 961fa8c418SNickeau $attributes->addClassName(self::RESPONSIVE_CLASS); 9737748cd8SNickeau 9837748cd8SNickeau 9937748cd8SNickeau /** 10037748cd8SNickeau * width and height to give the dimension ratio 10137748cd8SNickeau * They have an effect on the space reservation 10237748cd8SNickeau * but not on responsive image at all 10337748cd8SNickeau * To allow responsive height, the height style property is set at auto 10437748cd8SNickeau * (ie img-fluid in bootstrap) 10537748cd8SNickeau */ 10637748cd8SNickeau // The unit is not mandatory in HTML, this is expected to be CSS pixel 10737748cd8SNickeau // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 10837748cd8SNickeau // The HTML validator does not expect an unit otherwise it send an error 10937748cd8SNickeau // https://validator.w3.org/ 11037748cd8SNickeau $htmlLengthUnit = ""; 11137748cd8SNickeau 11237748cd8SNickeau /** 11337748cd8SNickeau * Height 11437748cd8SNickeau * The logical height that the image should take on the page 11537748cd8SNickeau * 11637748cd8SNickeau * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 11737748cd8SNickeau * 11837748cd8SNickeau */ 1191fa8c418SNickeau $targetHeight = $image->getTargetHeight(); 1201fa8c418SNickeau if (!empty($targetHeight)) { 1211fa8c418SNickeau $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit); 12237748cd8SNickeau } 12337748cd8SNickeau 12437748cd8SNickeau 12537748cd8SNickeau /** 12637748cd8SNickeau * Width 12737748cd8SNickeau * 12837748cd8SNickeau * We create a series of URL 12937748cd8SNickeau * for different width and let the browser 13037748cd8SNickeau * download the best one for: 13137748cd8SNickeau * * the actual container width 13237748cd8SNickeau * * the actual of screen resolution 13337748cd8SNickeau * * and the connection speed. 13437748cd8SNickeau * 13537748cd8SNickeau * The max-width value is set 13637748cd8SNickeau */ 1371fa8c418SNickeau $mediaWidthValue = $image->getIntrinsicWidth(); 1381fa8c418SNickeau $srcValue = $image->getUrl(); 13937748cd8SNickeau 14037748cd8SNickeau /** 14137748cd8SNickeau * Responsive image src set building 14237748cd8SNickeau * We have chosen 14337748cd8SNickeau * * 375: Iphone6 14437748cd8SNickeau * * 768: Ipad 14537748cd8SNickeau * * 1024: Ipad Pro 14637748cd8SNickeau * 14737748cd8SNickeau */ 14837748cd8SNickeau // The image margin applied 14937748cd8SNickeau $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 15037748cd8SNickeau 15137748cd8SNickeau 15237748cd8SNickeau /** 15337748cd8SNickeau * Srcset and sizes for responsive image 15437748cd8SNickeau * Width is mandatory for responsive image 15537748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 15637748cd8SNickeau */ 15737748cd8SNickeau if (!empty($mediaWidthValue)) { 15837748cd8SNickeau 15937748cd8SNickeau /** 1601fa8c418SNickeau * The value of the target image 16137748cd8SNickeau */ 1621fa8c418SNickeau $targetWidth = $image->getTargetWidth(); 1631fa8c418SNickeau if (!empty($targetWidth)) { 16437748cd8SNickeau 1651fa8c418SNickeau if (!empty($targetHeight)) { 1661fa8c418SNickeau $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 16737748cd8SNickeau } 1681fa8c418SNickeau $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit); 16937748cd8SNickeau } 17037748cd8SNickeau 17137748cd8SNickeau /** 17237748cd8SNickeau * Continue 17337748cd8SNickeau */ 17437748cd8SNickeau $srcSet = ""; 17537748cd8SNickeau $sizes = ""; 17637748cd8SNickeau 17737748cd8SNickeau /** 17837748cd8SNickeau * Add smaller sizes 17937748cd8SNickeau */ 18037748cd8SNickeau foreach (self::BREAKPOINTS as $breakpointWidth) { 18137748cd8SNickeau 1821fa8c418SNickeau if ($targetWidth > $breakpointWidth) { 18337748cd8SNickeau 18437748cd8SNickeau if (!empty($srcSet)) { 18537748cd8SNickeau $srcSet .= ", "; 18637748cd8SNickeau $sizes .= ", "; 18737748cd8SNickeau } 18837748cd8SNickeau $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin; 189*c3437056SNickeau $xsmUrl = $image->getUrlForSrcSetAtBreakpoint($breakpointWidthMinusMargin); 19037748cd8SNickeau $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w"; 19137748cd8SNickeau $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin); 19237748cd8SNickeau 19337748cd8SNickeau } 19437748cd8SNickeau 19537748cd8SNickeau } 19637748cd8SNickeau 19737748cd8SNickeau /** 19837748cd8SNickeau * Add the last size 1991fa8c418SNickeau * If the target image is really small, srcset and sizes are empty 20037748cd8SNickeau */ 20137748cd8SNickeau if (!empty($srcSet)) { 20237748cd8SNickeau $srcSet .= ", "; 20337748cd8SNickeau $sizes .= ", "; 204*c3437056SNickeau $srcUrl = $image->getUrlForSrcSetAtBreakpoint($targetWidth); 2051fa8c418SNickeau $srcSet .= "$srcUrl {$targetWidth}w"; 2061fa8c418SNickeau $sizes .= "{$targetWidth}px"; 20737748cd8SNickeau } 20837748cd8SNickeau 20937748cd8SNickeau /** 21037748cd8SNickeau * Lazy load 21137748cd8SNickeau */ 21237748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 21337748cd8SNickeau if ($lazyLoad) { 21437748cd8SNickeau 21537748cd8SNickeau /** 21637748cd8SNickeau * Snippet Lazy loading 21737748cd8SNickeau */ 21837748cd8SNickeau LazyLoad::addLozadSnippet(); 21937748cd8SNickeau PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster"); 2201fa8c418SNickeau $attributes->addClassName(self::LAZY_CLASS); 2211fa8c418SNickeau $attributes->addClassName(LazyLoad::LAZY_CLASS); 22237748cd8SNickeau 22337748cd8SNickeau /** 22437748cd8SNickeau * A small image has no srcset 22537748cd8SNickeau * 22637748cd8SNickeau */ 22737748cd8SNickeau if (!empty($srcSet)) { 22837748cd8SNickeau 22937748cd8SNickeau /** 23037748cd8SNickeau * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 23137748cd8SNickeau * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 23237748cd8SNickeau * The transparent image has a fix dimension aspect ratio of 1x1 making 23337748cd8SNickeau * a bad reserved space for the image 23437748cd8SNickeau * We use a svg instead 23537748cd8SNickeau */ 2361fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 2371fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 23837748cd8SNickeau /** 23937748cd8SNickeau * We use `data-sizes` and not `sizes` 24037748cd8SNickeau * because `sizes` without `srcset` 24137748cd8SNickeau * shows the broken image symbol 24237748cd8SNickeau * Javascript changes them at the same time 24337748cd8SNickeau */ 2441fa8c418SNickeau $attributes->addHtmlAttributeValue("data-sizes", $sizes); 2451fa8c418SNickeau $attributes->addHtmlAttributeValue("data-srcset", $srcSet); 24637748cd8SNickeau 24737748cd8SNickeau } else { 24837748cd8SNickeau 24937748cd8SNickeau /** 25037748cd8SNickeau * Small image but there is no little improvement 25137748cd8SNickeau */ 2521fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 253*c3437056SNickeau $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 25437748cd8SNickeau 25537748cd8SNickeau } 25637748cd8SNickeau 2571fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 25837748cd8SNickeau 25937748cd8SNickeau 26037748cd8SNickeau } else { 26137748cd8SNickeau 26237748cd8SNickeau if (!empty($srcSet)) { 2631fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", $srcSet); 2641fa8c418SNickeau $attributes->addHtmlAttributeValue("sizes", $sizes); 26537748cd8SNickeau } else { 2661fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 26737748cd8SNickeau } 26837748cd8SNickeau 26937748cd8SNickeau } 27037748cd8SNickeau 27137748cd8SNickeau } else { 27237748cd8SNickeau 27337748cd8SNickeau // No width, no responsive possibility 27437748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 27537748cd8SNickeau if ($lazyLoad) { 27637748cd8SNickeau 2771fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 2781fa8c418SNickeau $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder()); 2791fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 28037748cd8SNickeau 28137748cd8SNickeau } 28237748cd8SNickeau 28337748cd8SNickeau } 28437748cd8SNickeau 28537748cd8SNickeau 28637748cd8SNickeau /** 28737748cd8SNickeau * Title (ie alt) 28837748cd8SNickeau */ 2891fa8c418SNickeau $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty()); 2901fa8c418SNickeau 2911fa8c418SNickeau /** 2921fa8c418SNickeau * TODO: Side effect of the fact that we use the same attributes 2931fa8c418SNickeau * Title attribute of a media is the alt of an image 2941fa8c418SNickeau * And title should not be in an image tag 2951fa8c418SNickeau */ 2961fa8c418SNickeau $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY); 29737748cd8SNickeau 29837748cd8SNickeau /** 299*c3437056SNickeau * Old model where the src is parsed and the path 300*c3437056SNickeau * is in the attributes 301*c3437056SNickeau */ 302*c3437056SNickeau $attributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME); 303*c3437056SNickeau 304*c3437056SNickeau /** 30537748cd8SNickeau * Create the img element 30637748cd8SNickeau */ 3071fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 30837748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 30937748cd8SNickeau 31037748cd8SNickeau } else { 31137748cd8SNickeau 31237748cd8SNickeau $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>"; 31337748cd8SNickeau 31437748cd8SNickeau } 31537748cd8SNickeau 31637748cd8SNickeau return $imgHTML; 31737748cd8SNickeau } 31837748cd8SNickeau 31937748cd8SNickeau 32037748cd8SNickeau public 32137748cd8SNickeau function getLazyLoad() 32237748cd8SNickeau { 32337748cd8SNickeau $lazyLoad = parent::getLazyLoad(); 32437748cd8SNickeau if ($lazyLoad !== null) { 32537748cd8SNickeau return $lazyLoad; 32637748cd8SNickeau } else { 327*c3437056SNickeau return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE, RasterImageLink::CONF_LAZY_LOADING_ENABLE_DEFAULT); 32837748cd8SNickeau } 32937748cd8SNickeau } 33037748cd8SNickeau 33137748cd8SNickeau /** 33237748cd8SNickeau * @param $screenWidth 33337748cd8SNickeau * @param $imageWidth 33437748cd8SNickeau * @return string sizes with a dpi correction if 33537748cd8SNickeau */ 33637748cd8SNickeau private 3371fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 33837748cd8SNickeau { 33937748cd8SNickeau 34037748cd8SNickeau if ($this->getWithDpiCorrection()) { 34137748cd8SNickeau $dpiBase = 96; 34237748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 34337748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 34437748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 34537748cd8SNickeau } else { 34637748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 34737748cd8SNickeau } 34837748cd8SNickeau return $sizes; 34937748cd8SNickeau } 35037748cd8SNickeau 35137748cd8SNickeau /** 35237748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 35337748cd8SNickeau * 35437748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 35537748cd8SNickeau * 35637748cd8SNickeau * This can be disturbing when debugging responsive sizing image 35737748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 35837748cd8SNickeau * 35937748cd8SNickeau * @return bool 36037748cd8SNickeau */ 36137748cd8SNickeau private 3621fa8c418SNickeau function getWithDpiCorrection(): bool 36337748cd8SNickeau { 36437748cd8SNickeau /** 36537748cd8SNickeau * Support for retina means no DPI correction 36637748cd8SNickeau */ 36737748cd8SNickeau $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 36837748cd8SNickeau return !$retinaEnabled; 36937748cd8SNickeau } 37037748cd8SNickeau 37137748cd8SNickeau 37237748cd8SNickeau} 373