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 1504fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 1604fd306cSNickeau 1737748cd8SNickeaurequire_once(__DIR__ . '/MediaLink.php'); 1837748cd8SNickeaurequire_once(__DIR__ . '/LazyLoad.php'); 1937748cd8SNickeaurequire_once(__DIR__ . '/PluginUtility.php'); 2037748cd8SNickeau 2137748cd8SNickeau/** 2237748cd8SNickeau * Image 2337748cd8SNickeau * This is the class that handles the 2437748cd8SNickeau * raster image type of the dokuwiki {@link MediaLink} 2537748cd8SNickeau * 2637748cd8SNickeau * The real documentation can be found on the image page 2737748cd8SNickeau * @link https://www.dokuwiki.org/images 2837748cd8SNickeau * 2937748cd8SNickeau * Doc: 3037748cd8SNickeau * https://web.dev/optimize-cls/#images-without-dimensions 3137748cd8SNickeau * https://web.dev/cls/ 3237748cd8SNickeau */ 331fa8c418SNickeauclass RasterImageLink extends ImageLink 3437748cd8SNickeau{ 3537748cd8SNickeau 3604fd306cSNickeau const CANONICAL = FetcherRaster::CANONICAL; 3737748cd8SNickeau 3837748cd8SNickeau const RESPONSIVE_CLASS = "img-fluid"; 3937748cd8SNickeau 4037748cd8SNickeau const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin"; 4137748cd8SNickeau const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable"; 4237748cd8SNickeau 4304fd306cSNickeau private FetcherImage $fetchRaster; 4437748cd8SNickeau 4537748cd8SNickeau 4637748cd8SNickeau /** 4704fd306cSNickeau * @throws ExceptionBadArgument - if the fetcher is not a raster mime and image fetcher 4837748cd8SNickeau */ 4904fd306cSNickeau public function __construct(MediaMarkup $mediaMarkup) 5037748cd8SNickeau { 5104fd306cSNickeau $fetcher = $mediaMarkup->getFetcher(); 5204fd306cSNickeau $mime = $fetcher->getMime(); 5304fd306cSNickeau if (!$mime->isSupportedRasterImage()) { 5404fd306cSNickeau throw new ExceptionBadArgument("The mime value ($mime) is not a supported raster image.", self::CANONICAL); 5504fd306cSNickeau } 5604fd306cSNickeau if (!($fetcher instanceof FetcherImage)) { 5704fd306cSNickeau throw new ExceptionBadArgument("The fetcher is not a fetcher image but is a " . get_class($fetcher)); 5804fd306cSNickeau } 5904fd306cSNickeau $this->fetchRaster = $fetcher; 6004fd306cSNickeau parent::__construct($mediaMarkup); 6137748cd8SNickeau } 6237748cd8SNickeau 6337748cd8SNickeau 6437748cd8SNickeau /** 6537748cd8SNickeau * Render a link 6637748cd8SNickeau * Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()} 6737748cd8SNickeau * A media can be a video also (Use 6837748cd8SNickeau * @return string 6904fd306cSNickeau * @throws ExceptionNotFound 7037748cd8SNickeau */ 711fa8c418SNickeau public function renderMediaTag(): string 7237748cd8SNickeau { 7337748cd8SNickeau 7437748cd8SNickeau 7504fd306cSNickeau $fetchRaster = $this->fetchRaster; 7604fd306cSNickeau 7704fd306cSNickeau $attributes = $this->mediaMarkup->getExtraMediaTagAttributes() 7804fd306cSNickeau ->setLogicalTag(self::CANONICAL); 7937748cd8SNickeau 8037748cd8SNickeau /** 8137748cd8SNickeau * Responsive image 8237748cd8SNickeau * https://getbootstrap.com/docs/5.0/content/images/ 8337748cd8SNickeau * to apply max-width: 100%; and height: auto; 8437748cd8SNickeau * 8537748cd8SNickeau * Even if the resizing is requested by height, 8637748cd8SNickeau * the height: auto on styling is needed to conserve the ratio 8737748cd8SNickeau * while scaling down the screen 8837748cd8SNickeau */ 891fa8c418SNickeau $attributes->addClassName(self::RESPONSIVE_CLASS); 9037748cd8SNickeau 9137748cd8SNickeau 9237748cd8SNickeau /** 9337748cd8SNickeau * width and height to give the dimension ratio 9437748cd8SNickeau * They have an effect on the space reservation 9537748cd8SNickeau * but not on responsive image at all 9637748cd8SNickeau * To allow responsive height, the height style property is set at auto 9737748cd8SNickeau * (ie img-fluid in bootstrap) 9837748cd8SNickeau */ 9937748cd8SNickeau // The unit is not mandatory in HTML, this is expected to be CSS pixel 10037748cd8SNickeau // https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 10137748cd8SNickeau // The HTML validator does not expect an unit otherwise it send an error 10237748cd8SNickeau // https://validator.w3.org/ 10337748cd8SNickeau $htmlLengthUnit = ""; 10482a60d03SNickeau $cssLengthUnit = "px"; 10537748cd8SNickeau 10637748cd8SNickeau /** 10737748cd8SNickeau * Height 10837748cd8SNickeau * The logical height that the image should take on the page 10937748cd8SNickeau * 11037748cd8SNickeau * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 11137748cd8SNickeau * 11204fd306cSNickeau * Cannot be empty 11337748cd8SNickeau */ 11404fd306cSNickeau $targetHeight = $fetchRaster->getTargetHeight(); 11504fd306cSNickeau 11682a60d03SNickeau /** 11782a60d03SNickeau * HTML height attribute is important for the ratio calculation 11882a60d03SNickeau * No layout shift 11982a60d03SNickeau */ 1204cadd4f8SNickeau $attributes->addOutputAttributeValue("height", $targetHeight . $htmlLengthUnit); 12182a60d03SNickeau /** 12282a60d03SNickeau * We don't allow the image to scale up by default 12382a60d03SNickeau */ 12482a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit); 1254cadd4f8SNickeau /** 1264cadd4f8SNickeau * if the image has a class that has a `height: 100%`, the image will stretch 1274cadd4f8SNickeau */ 1284cadd4f8SNickeau $attributes->addStyleDeclarationIfNotSet("height", "auto"); 12937748cd8SNickeau 13037748cd8SNickeau 13137748cd8SNickeau /** 13237748cd8SNickeau * Responsive image src set building 13337748cd8SNickeau * We have chosen 13437748cd8SNickeau * * 375: Iphone6 13537748cd8SNickeau * * 768: Ipad 13637748cd8SNickeau * * 1024: Ipad Pro 13737748cd8SNickeau * 13837748cd8SNickeau */ 13937748cd8SNickeau // The image margin applied 14004fd306cSNickeau $imageMargin = SiteConfig::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 141*70bbd7f1Sgerardnico try { 142*70bbd7f1Sgerardnico $imageMargin = ConditionalLength::createFromString($imageMargin)->toPixelNumber(); 143*70bbd7f1Sgerardnico } catch (ExceptionBadArgument $e) { 144*70bbd7f1Sgerardnico LogUtility::warning("The variable (" . self::CONF_RESPONSIVE_IMAGE_MARGIN . ") has a value ($imageMargin) that is not a valid length.", self::CANONICAL, $e); 145*70bbd7f1Sgerardnico $imageMargin = 20; 146*70bbd7f1Sgerardnico } 14737748cd8SNickeau 14837748cd8SNickeau /** 14937748cd8SNickeau * Srcset and sizes for responsive image 15037748cd8SNickeau * Width is mandatory for responsive image 15137748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 15237748cd8SNickeau */ 15304fd306cSNickeau 15437748cd8SNickeau 15537748cd8SNickeau /** 1561fa8c418SNickeau * The value of the target image 15737748cd8SNickeau */ 15804fd306cSNickeau $targetWidth = $fetchRaster->getTargetWidth(); 15904fd306cSNickeau $fetchRaster->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 16037748cd8SNickeau 16182a60d03SNickeau /** 16282a60d03SNickeau * HTML Width attribute is important to avoid layout shift 16382a60d03SNickeau */ 1644cadd4f8SNickeau $attributes->addOutputAttributeValue("width", $targetWidth . $htmlLengthUnit); 16582a60d03SNickeau /** 16682a60d03SNickeau * We don't allow the image to scale up by default 16782a60d03SNickeau */ 16882a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit); 16982a60d03SNickeau /** 17082a60d03SNickeau * We allow the image to scale down up to 100% of its parent 17182a60d03SNickeau */ 17282a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("width", "100%"); 17382a60d03SNickeau 17437748cd8SNickeau 17537748cd8SNickeau /** 17637748cd8SNickeau * Continue 17737748cd8SNickeau */ 17837748cd8SNickeau $srcSet = ""; 17937748cd8SNickeau $sizes = ""; 18037748cd8SNickeau 18137748cd8SNickeau /** 18204fd306cSNickeau * Width 18304fd306cSNickeau * 18404fd306cSNickeau * We create a series of URL 18504fd306cSNickeau * for different width and let the browser 18604fd306cSNickeau * download the best one for: 18704fd306cSNickeau * * the actual container width 18804fd306cSNickeau * * the actual of screen resolution 18904fd306cSNickeau * * and the connection speed. 19004fd306cSNickeau * 19104fd306cSNickeau * The max-width value is set 19237748cd8SNickeau */ 19304fd306cSNickeau $srcValue = $fetchRaster->getFetchUrl(); 19404fd306cSNickeau /** 19504fd306cSNickeau * Add samller breakpoints sizes 19604fd306cSNickeau */ 19704fd306cSNickeau $intrinsicWidth = $fetchRaster->getIntrinsicWidth(); 19804fd306cSNickeau foreach (Breakpoint::getBreakpoints() as $breakpoint) { 19937748cd8SNickeau 20004fd306cSNickeau try { 20104fd306cSNickeau $breakpointPixels = $breakpoint->getWidth(); 20204fd306cSNickeau } catch (ExceptionInfinite $e) { 20304fd306cSNickeau continue; 20404fd306cSNickeau } 20504fd306cSNickeau 20604fd306cSNickeau if ($breakpointPixels > $targetWidth) { 20704fd306cSNickeau continue; 20804fd306cSNickeau } 20904fd306cSNickeau 21004fd306cSNickeau if ($breakpointPixels > $intrinsicWidth) { 21104fd306cSNickeau continue; 21204fd306cSNickeau } 21337748cd8SNickeau 21437748cd8SNickeau if (!empty($srcSet)) { 21537748cd8SNickeau $srcSet .= ", "; 21637748cd8SNickeau $sizes .= ", "; 21737748cd8SNickeau } 21804fd306cSNickeau $breakpointWidthMinusMargin = $breakpointPixels - $imageMargin; 2194cadd4f8SNickeau 22037748cd8SNickeau 22104fd306cSNickeau $breakpointRaster = clone $fetchRaster; 22204fd306cSNickeau if ( 22304fd306cSNickeau !$fetchRaster->hasHeightRequested() // breakpoint url needs only the h attribute in this case 22404fd306cSNickeau || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory 22504fd306cSNickeau ) { 22604fd306cSNickeau $breakpointRaster->setRequestedWidth($breakpointWidthMinusMargin); 22737748cd8SNickeau } 22804fd306cSNickeau if ($fetchRaster->hasHeightRequested() // if this is a height request 22904fd306cSNickeau || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory 23004fd306cSNickeau ) { 23104fd306cSNickeau $breakPointHeight = FetcherRaster::round($breakpointWidthMinusMargin / $fetchRaster->getTargetAspectRatio()); 23204fd306cSNickeau $breakpointRaster->setRequestedHeight($breakPointHeight); 23304fd306cSNickeau } 23404fd306cSNickeau 23504fd306cSNickeau $breakpointUrl = $breakpointRaster->getFetchUrl()->toString(); 23604fd306cSNickeau 23704fd306cSNickeau 23804fd306cSNickeau $srcSet .= "$breakpointUrl {$breakpointWidthMinusMargin}w"; 23904fd306cSNickeau $sizes .= $this->getSizes($breakpointPixels, $breakpointWidthMinusMargin); 24004fd306cSNickeau 24137748cd8SNickeau 24237748cd8SNickeau } 24337748cd8SNickeau 24437748cd8SNickeau /** 24537748cd8SNickeau * Add the last size 2461fa8c418SNickeau * If the target image is really small, srcset and sizes are empty 24737748cd8SNickeau */ 24837748cd8SNickeau if (!empty($srcSet)) { 24937748cd8SNickeau $srcSet .= ", "; 25037748cd8SNickeau $sizes .= ", "; 25104fd306cSNickeau $srcUrl = $fetchRaster->getFetchUrl()->toString(); 2521fa8c418SNickeau $srcSet .= "$srcUrl {$targetWidth}w"; 2531fa8c418SNickeau $sizes .= "{$targetWidth}px"; 25437748cd8SNickeau } 25537748cd8SNickeau 25637748cd8SNickeau /** 25737748cd8SNickeau * Lazy load 25837748cd8SNickeau */ 25937748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 26037748cd8SNickeau if ($lazyLoad) { 26137748cd8SNickeau 26237748cd8SNickeau /** 2634cadd4f8SNickeau * Html Lazy loading 2644cadd4f8SNickeau */ 26504fd306cSNickeau $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault(); 2664cadd4f8SNickeau switch ($lazyLoadMethod) { 26704fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE: 26804fd306cSNickeau default: 2694cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 2704cadd4f8SNickeau if (!empty($srcSet)) { 2714cadd4f8SNickeau // it the image is small, no srcset for instance 2724cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 2734cadd4f8SNickeau } 2744cadd4f8SNickeau $attributes->addOutputAttributeValue("loading", "lazy"); 2754cadd4f8SNickeau break; 27604fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE: 2774cadd4f8SNickeau /** 27837748cd8SNickeau * Snippet Lazy loading 27937748cd8SNickeau */ 28037748cd8SNickeau LazyLoad::addLozadSnippet(); 28104fd306cSNickeau PluginUtility::getSnippetManager()->attachJavascriptFromComponentId("lozad-raster"); 28204fd306cSNickeau $attributes->addClassName(self::getLazyClass()); 28304fd306cSNickeau $attributes->addClassName(LazyLoad::getLazyClass()); 28437748cd8SNickeau 28537748cd8SNickeau /** 28637748cd8SNickeau * A small image has no srcset 28737748cd8SNickeau * 28837748cd8SNickeau */ 28937748cd8SNickeau if (!empty($srcSet)) { 29037748cd8SNickeau 29137748cd8SNickeau /** 29237748cd8SNickeau * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 29337748cd8SNickeau * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 29437748cd8SNickeau * The transparent image has a fix dimension aspect ratio of 1x1 making 29537748cd8SNickeau * a bad reserved space for the image 29637748cd8SNickeau * We use a svg instead 29737748cd8SNickeau */ 2984cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 2994cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 30037748cd8SNickeau /** 30137748cd8SNickeau * We use `data-sizes` and not `sizes` 30237748cd8SNickeau * because `sizes` without `srcset` 30337748cd8SNickeau * shows the broken image symbol 30437748cd8SNickeau * Javascript changes them at the same time 30537748cd8SNickeau */ 3064cadd4f8SNickeau $attributes->addOutputAttributeValue("data-sizes", $sizes); 3074cadd4f8SNickeau $attributes->addOutputAttributeValue("data-srcset", $srcSet); 30837748cd8SNickeau 30937748cd8SNickeau } else { 31037748cd8SNickeau 31137748cd8SNickeau /** 31237748cd8SNickeau * Small image but there is no little improvement 31337748cd8SNickeau */ 3144cadd4f8SNickeau $attributes->addOutputAttributeValue("data-src", $srcValue); 3154cadd4f8SNickeau $attributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 31637748cd8SNickeau 31737748cd8SNickeau } 3181fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 3194cadd4f8SNickeau break; 3204cadd4f8SNickeau } 32137748cd8SNickeau 32237748cd8SNickeau 32337748cd8SNickeau } else { 32437748cd8SNickeau 32537748cd8SNickeau if (!empty($srcSet)) { 3264cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 3274cadd4f8SNickeau $attributes->addOutputAttributeValue("sizes", $sizes); 32837748cd8SNickeau } else { 3294cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 33037748cd8SNickeau } 33137748cd8SNickeau 33237748cd8SNickeau } 33337748cd8SNickeau 33437748cd8SNickeau 33537748cd8SNickeau /** 33637748cd8SNickeau * Title (ie alt) 33737748cd8SNickeau */ 33804fd306cSNickeau $attributes->addOutputAttributeValueIfNotEmpty("alt", $this->getAltNotEmpty()); 339c3437056SNickeau 340c3437056SNickeau /** 34137748cd8SNickeau * Create the img element 34237748cd8SNickeau */ 3431fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 34437748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 34537748cd8SNickeau 34637748cd8SNickeau 34704fd306cSNickeau return $this->wrapMediaMarkupWithLink($imgHTML); 34837748cd8SNickeau } 34937748cd8SNickeau 35037748cd8SNickeau 35104fd306cSNickeau public function getLazyLoad(): bool 35237748cd8SNickeau { 35304fd306cSNickeau 35404fd306cSNickeau if ($this->mediaMarkup->isLazy() === false) { 35504fd306cSNickeau return false; 35637748cd8SNickeau } 35704fd306cSNickeau return SiteConfig::getConfValue(LazyLoad::CONF_RASTER_ENABLE, LazyLoad::CONF_RASTER_ENABLE_DEFAULT); 35804fd306cSNickeau 35937748cd8SNickeau } 36037748cd8SNickeau 36137748cd8SNickeau /** 36237748cd8SNickeau * @param $screenWidth 36337748cd8SNickeau * @param $imageWidth 36437748cd8SNickeau * @return string sizes with a dpi correction if 36537748cd8SNickeau */ 36637748cd8SNickeau private 3671fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 36837748cd8SNickeau { 36937748cd8SNickeau 37037748cd8SNickeau if ($this->getWithDpiCorrection()) { 37137748cd8SNickeau $dpiBase = 96; 37237748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 37337748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 37437748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 37537748cd8SNickeau } else { 37637748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 37737748cd8SNickeau } 37837748cd8SNickeau return $sizes; 37937748cd8SNickeau } 38037748cd8SNickeau 38137748cd8SNickeau /** 38237748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 38337748cd8SNickeau * 38437748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 38537748cd8SNickeau * 38637748cd8SNickeau * This can be disturbing when debugging responsive sizing image 38737748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 38837748cd8SNickeau * 38937748cd8SNickeau * @return bool 39037748cd8SNickeau */ 39137748cd8SNickeau private 3921fa8c418SNickeau function getWithDpiCorrection(): bool 39337748cd8SNickeau { 39437748cd8SNickeau /** 39537748cd8SNickeau * Support for retina means no DPI correction 39637748cd8SNickeau */ 39704fd306cSNickeau $retinaEnabled = SiteConfig::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 39837748cd8SNickeau return !$retinaEnabled; 39937748cd8SNickeau } 40037748cd8SNickeau 40104fd306cSNickeau /** 40204fd306cSNickeau * Used to select the raster image lazy loaded 40304fd306cSNickeau * @return string 40404fd306cSNickeau */ 40504fd306cSNickeau public static function getLazyClass() 40604fd306cSNickeau { 40704fd306cSNickeau return StyleAttribute::addComboStrapSuffix("lazy-raster"); 40804fd306cSNickeau } 40904fd306cSNickeau 41037748cd8SNickeau 41137748cd8SNickeau} 412