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"; 36c3437056SNickeau 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 70*4cadd4f8SNickeau * @throws ExceptionCombo 7137748cd8SNickeau */ 721fa8c418SNickeau public function renderMediaTag(): string 7337748cd8SNickeau { 74c3437056SNickeau /** 75c3437056SNickeau * @var ImageRaster $image 76c3437056SNickeau */ 771fa8c418SNickeau $image = $this->getDefaultImage(); 781fa8c418SNickeau if ($image->exists()) { 7937748cd8SNickeau 801fa8c418SNickeau $attributes = $image->getAttributes(); 8137748cd8SNickeau 8237748cd8SNickeau /** 8337748cd8SNickeau * No dokuwiki type attribute 8437748cd8SNickeau */ 851fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE); 861fa8c418SNickeau $attributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC); 8737748cd8SNickeau 8837748cd8SNickeau /** 8937748cd8SNickeau * Responsive image 9037748cd8SNickeau * https://getbootstrap.com/docs/5.0/content/images/ 9137748cd8SNickeau * to apply max-width: 100%; and height: auto; 9237748cd8SNickeau * 9337748cd8SNickeau * Even if the resizing is requested by height, 9437748cd8SNickeau * the height: auto on styling is needed to conserve the ratio 9537748cd8SNickeau * while scaling down the screen 9637748cd8SNickeau */ 971fa8c418SNickeau $attributes->addClassName(self::RESPONSIVE_CLASS); 9837748cd8SNickeau 9937748cd8SNickeau 10037748cd8SNickeau /** 10137748cd8SNickeau * width and height to give the dimension ratio 10237748cd8SNickeau * They have an effect on the space reservation 10337748cd8SNickeau * but not on responsive image at all 10437748cd8SNickeau * To allow responsive height, the height style property is set at auto 10537748cd8SNickeau * (ie img-fluid in bootstrap) 10637748cd8SNickeau */ 10737748cd8SNickeau // The unit is not mandatory in HTML, this is expected to be CSS pixel 10837748cd8SNickeau // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 10937748cd8SNickeau // The HTML validator does not expect an unit otherwise it send an error 11037748cd8SNickeau // https://validator.w3.org/ 11137748cd8SNickeau $htmlLengthUnit = ""; 11282a60d03SNickeau $cssLengthUnit = "px"; 11337748cd8SNickeau 11437748cd8SNickeau /** 11537748cd8SNickeau * Height 11637748cd8SNickeau * The logical height that the image should take on the page 11737748cd8SNickeau * 11837748cd8SNickeau * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 11937748cd8SNickeau * 12037748cd8SNickeau */ 12182a60d03SNickeau try { 1221fa8c418SNickeau $targetHeight = $image->getTargetHeight(); 12382a60d03SNickeau } catch (ExceptionCombo $e) { 12482a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The target height reports a problem: {$e->getMessage()}"); 12582a60d03SNickeau return ""; 12682a60d03SNickeau } 1271fa8c418SNickeau if (!empty($targetHeight)) { 12882a60d03SNickeau /** 12982a60d03SNickeau * HTML height attribute is important for the ratio calculation 13082a60d03SNickeau * No layout shift 13182a60d03SNickeau */ 132*4cadd4f8SNickeau $attributes->addOutputAttributeValue("height", $targetHeight . $htmlLengthUnit); 13382a60d03SNickeau /** 13482a60d03SNickeau * We don't allow the image to scale up by default 13582a60d03SNickeau */ 13682a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit); 137*4cadd4f8SNickeau /** 138*4cadd4f8SNickeau * if the image has a class that has a `height: 100%`, the image will stretch 139*4cadd4f8SNickeau */ 140*4cadd4f8SNickeau $attributes->addStyleDeclarationIfNotSet("height", "auto"); 14137748cd8SNickeau } 14237748cd8SNickeau 14337748cd8SNickeau 14437748cd8SNickeau /** 14537748cd8SNickeau * Width 14637748cd8SNickeau * 14737748cd8SNickeau * We create a series of URL 14837748cd8SNickeau * for different width and let the browser 14937748cd8SNickeau * download the best one for: 15037748cd8SNickeau * * the actual container width 15137748cd8SNickeau * * the actual of screen resolution 15237748cd8SNickeau * * and the connection speed. 15337748cd8SNickeau * 15437748cd8SNickeau * The max-width value is set 15537748cd8SNickeau */ 15682a60d03SNickeau try { 1571fa8c418SNickeau $mediaWidthValue = $image->getIntrinsicWidth(); 15882a60d03SNickeau } catch (ExceptionCombo $e) { 15982a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The intrinsic width reports a problem: {$e->getMessage()}"); 16082a60d03SNickeau return ""; 16182a60d03SNickeau } 1621fa8c418SNickeau $srcValue = $image->getUrl(); 16337748cd8SNickeau 16437748cd8SNickeau /** 16537748cd8SNickeau * Responsive image src set building 16637748cd8SNickeau * We have chosen 16737748cd8SNickeau * * 375: Iphone6 16837748cd8SNickeau * * 768: Ipad 16937748cd8SNickeau * * 1024: Ipad Pro 17037748cd8SNickeau * 17137748cd8SNickeau */ 17237748cd8SNickeau // The image margin applied 17337748cd8SNickeau $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 17437748cd8SNickeau 17537748cd8SNickeau 17637748cd8SNickeau /** 17737748cd8SNickeau * Srcset and sizes for responsive image 17837748cd8SNickeau * Width is mandatory for responsive image 17937748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 18037748cd8SNickeau */ 18137748cd8SNickeau if (!empty($mediaWidthValue)) { 18237748cd8SNickeau 18337748cd8SNickeau /** 1841fa8c418SNickeau * The value of the target image 18537748cd8SNickeau */ 18682a60d03SNickeau try { 1871fa8c418SNickeau $targetWidth = $image->getTargetWidth(); 18882a60d03SNickeau } catch (ExceptionCombo $e) { 18982a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The target width reports a problem: {$e->getMessage()}"); 19082a60d03SNickeau return ""; 19182a60d03SNickeau } 1921fa8c418SNickeau if (!empty($targetWidth)) { 19337748cd8SNickeau 1941fa8c418SNickeau if (!empty($targetHeight)) { 1951fa8c418SNickeau $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 19637748cd8SNickeau } 19782a60d03SNickeau /** 19882a60d03SNickeau * HTML Width attribute is important to avoid layout shift 19982a60d03SNickeau */ 200*4cadd4f8SNickeau $attributes->addOutputAttributeValue("width", $targetWidth . $htmlLengthUnit); 20182a60d03SNickeau /** 20282a60d03SNickeau * We don't allow the image to scale up by default 20382a60d03SNickeau */ 20482a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit); 20582a60d03SNickeau /** 20682a60d03SNickeau * We allow the image to scale down up to 100% of its parent 20782a60d03SNickeau */ 20882a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("width", "100%"); 20982a60d03SNickeau 21037748cd8SNickeau } 21137748cd8SNickeau 21237748cd8SNickeau /** 21337748cd8SNickeau * Continue 21437748cd8SNickeau */ 21537748cd8SNickeau $srcSet = ""; 21637748cd8SNickeau $sizes = ""; 21737748cd8SNickeau 21837748cd8SNickeau /** 21937748cd8SNickeau * Add smaller sizes 22037748cd8SNickeau */ 22137748cd8SNickeau foreach (self::BREAKPOINTS as $breakpointWidth) { 22237748cd8SNickeau 2231fa8c418SNickeau if ($targetWidth > $breakpointWidth) { 22437748cd8SNickeau 22537748cd8SNickeau if (!empty($srcSet)) { 22637748cd8SNickeau $srcSet .= ", "; 22737748cd8SNickeau $sizes .= ", "; 22837748cd8SNickeau } 22937748cd8SNickeau $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin; 230*4cadd4f8SNickeau 231*4cadd4f8SNickeau $xsmUrl = $image->getUrlAtBreakpoint($breakpointWidthMinusMargin); 23237748cd8SNickeau $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w"; 23337748cd8SNickeau $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin); 23437748cd8SNickeau 23537748cd8SNickeau } 23637748cd8SNickeau 23737748cd8SNickeau } 23837748cd8SNickeau 23937748cd8SNickeau /** 24037748cd8SNickeau * Add the last size 2411fa8c418SNickeau * If the target image is really small, srcset and sizes are empty 24237748cd8SNickeau */ 24337748cd8SNickeau if (!empty($srcSet)) { 24437748cd8SNickeau $srcSet .= ", "; 24537748cd8SNickeau $sizes .= ", "; 246*4cadd4f8SNickeau $srcUrl = $image->getUrlAtBreakpoint($targetWidth); 2471fa8c418SNickeau $srcSet .= "$srcUrl {$targetWidth}w"; 2481fa8c418SNickeau $sizes .= "{$targetWidth}px"; 24937748cd8SNickeau } 25037748cd8SNickeau 25137748cd8SNickeau /** 25237748cd8SNickeau * Lazy load 25337748cd8SNickeau */ 25437748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 25537748cd8SNickeau if ($lazyLoad) { 25637748cd8SNickeau 25737748cd8SNickeau /** 258*4cadd4f8SNickeau * Html Lazy loading 259*4cadd4f8SNickeau */ 260*4cadd4f8SNickeau $lazyLoadMethod = $this->getLazyLoadMethod(); 261*4cadd4f8SNickeau switch ($lazyLoadMethod) { 262*4cadd4f8SNickeau case MediaLink::LAZY_LOAD_METHOD_HTML_VALUE: 263*4cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 264*4cadd4f8SNickeau if (!empty($srcSet)) { 265*4cadd4f8SNickeau // it the image is small, no srcset for instance 266*4cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 267*4cadd4f8SNickeau } 268*4cadd4f8SNickeau $attributes->addOutputAttributeValue("loading", "lazy"); 269*4cadd4f8SNickeau break; 270*4cadd4f8SNickeau default: 271*4cadd4f8SNickeau case MediaLink::LAZY_LOAD_METHOD_LOZAD_VALUE: 272*4cadd4f8SNickeau /** 27337748cd8SNickeau * Snippet Lazy loading 27437748cd8SNickeau */ 27537748cd8SNickeau LazyLoad::addLozadSnippet(); 276*4cadd4f8SNickeau PluginUtility::getSnippetManager()->attachInternalJavascriptForSlot("lozad-raster"); 2771fa8c418SNickeau $attributes->addClassName(self::LAZY_CLASS); 2781fa8c418SNickeau $attributes->addClassName(LazyLoad::LAZY_CLASS); 27937748cd8SNickeau 28037748cd8SNickeau /** 28137748cd8SNickeau * A small image has no srcset 28237748cd8SNickeau * 28337748cd8SNickeau */ 28437748cd8SNickeau if (!empty($srcSet)) { 28537748cd8SNickeau 28637748cd8SNickeau /** 28737748cd8SNickeau * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 28837748cd8SNickeau * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 28937748cd8SNickeau * The transparent image has a fix dimension aspect ratio of 1x1 making 29037748cd8SNickeau * a bad reserved space for the image 29137748cd8SNickeau * We use a svg instead 29237748cd8SNickeau */ 293*4cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 294*4cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 29537748cd8SNickeau /** 29637748cd8SNickeau * We use `data-sizes` and not `sizes` 29737748cd8SNickeau * because `sizes` without `srcset` 29837748cd8SNickeau * shows the broken image symbol 29937748cd8SNickeau * Javascript changes them at the same time 30037748cd8SNickeau */ 301*4cadd4f8SNickeau $attributes->addOutputAttributeValue("data-sizes", $sizes); 302*4cadd4f8SNickeau $attributes->addOutputAttributeValue("data-srcset", $srcSet); 30337748cd8SNickeau 30437748cd8SNickeau } else { 30537748cd8SNickeau 30637748cd8SNickeau /** 30737748cd8SNickeau * Small image but there is no little improvement 30837748cd8SNickeau */ 309*4cadd4f8SNickeau $attributes->addOutputAttributeValue("data-src", $srcValue); 310*4cadd4f8SNickeau $attributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 31137748cd8SNickeau 31237748cd8SNickeau } 3131fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 314*4cadd4f8SNickeau break; 315*4cadd4f8SNickeau } 31637748cd8SNickeau 31737748cd8SNickeau 31837748cd8SNickeau } else { 31937748cd8SNickeau 32037748cd8SNickeau if (!empty($srcSet)) { 321*4cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 322*4cadd4f8SNickeau $attributes->addOutputAttributeValue("sizes", $sizes); 32337748cd8SNickeau } else { 324*4cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 32537748cd8SNickeau } 32637748cd8SNickeau 32737748cd8SNickeau } 32837748cd8SNickeau 32937748cd8SNickeau } else { 33037748cd8SNickeau 33137748cd8SNickeau // No width, no responsive possibility 33237748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 33337748cd8SNickeau if ($lazyLoad) { 33437748cd8SNickeau 3351fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 336*4cadd4f8SNickeau $attributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder()); 337*4cadd4f8SNickeau $attributes->addOutputAttributeValue("data-src", $srcValue); 33837748cd8SNickeau 33937748cd8SNickeau } 34037748cd8SNickeau 34137748cd8SNickeau } 34237748cd8SNickeau 34337748cd8SNickeau 34437748cd8SNickeau /** 34537748cd8SNickeau * Title (ie alt) 34637748cd8SNickeau */ 347*4cadd4f8SNickeau $attributes->addOutputAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty()); 3481fa8c418SNickeau 3491fa8c418SNickeau /** 3501fa8c418SNickeau * TODO: Side effect of the fact that we use the same attributes 3511fa8c418SNickeau * Title attribute of a media is the alt of an image 3521fa8c418SNickeau * And title should not be in an image tag 3531fa8c418SNickeau */ 3541fa8c418SNickeau $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY); 35537748cd8SNickeau 35637748cd8SNickeau /** 357c3437056SNickeau * Old model where the src is parsed and the path 358c3437056SNickeau * is in the attributes 359c3437056SNickeau */ 360c3437056SNickeau $attributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME); 361c3437056SNickeau 362c3437056SNickeau /** 36337748cd8SNickeau * Create the img element 36437748cd8SNickeau */ 3651fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 36637748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 36737748cd8SNickeau 36837748cd8SNickeau } else { 36937748cd8SNickeau 37037748cd8SNickeau $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>"; 37137748cd8SNickeau 37237748cd8SNickeau } 37337748cd8SNickeau 37437748cd8SNickeau return $imgHTML; 37537748cd8SNickeau } 37637748cd8SNickeau 37737748cd8SNickeau 37837748cd8SNickeau public 37937748cd8SNickeau function getLazyLoad() 38037748cd8SNickeau { 38137748cd8SNickeau $lazyLoad = parent::getLazyLoad(); 38237748cd8SNickeau if ($lazyLoad !== null) { 38337748cd8SNickeau return $lazyLoad; 38437748cd8SNickeau } else { 385c3437056SNickeau return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE, RasterImageLink::CONF_LAZY_LOADING_ENABLE_DEFAULT); 38637748cd8SNickeau } 38737748cd8SNickeau } 38837748cd8SNickeau 38937748cd8SNickeau /** 39037748cd8SNickeau * @param $screenWidth 39137748cd8SNickeau * @param $imageWidth 39237748cd8SNickeau * @return string sizes with a dpi correction if 39337748cd8SNickeau */ 39437748cd8SNickeau private 3951fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 39637748cd8SNickeau { 39737748cd8SNickeau 39837748cd8SNickeau if ($this->getWithDpiCorrection()) { 39937748cd8SNickeau $dpiBase = 96; 40037748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 40137748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 40237748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 40337748cd8SNickeau } else { 40437748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 40537748cd8SNickeau } 40637748cd8SNickeau return $sizes; 40737748cd8SNickeau } 40837748cd8SNickeau 40937748cd8SNickeau /** 41037748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 41137748cd8SNickeau * 41237748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 41337748cd8SNickeau * 41437748cd8SNickeau * This can be disturbing when debugging responsive sizing image 41537748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 41637748cd8SNickeau * 41737748cd8SNickeau * @return bool 41837748cd8SNickeau */ 41937748cd8SNickeau private 4201fa8c418SNickeau function getWithDpiCorrection(): bool 42137748cd8SNickeau { 42237748cd8SNickeau /** 42337748cd8SNickeau * Support for retina means no DPI correction 42437748cd8SNickeau */ 42537748cd8SNickeau $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 42637748cd8SNickeau return !$retinaEnabled; 42737748cd8SNickeau } 42837748cd8SNickeau 42937748cd8SNickeau 43037748cd8SNickeau} 431