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 15*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 16*04fd306cSNickeau 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 36*04fd306cSNickeau 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 43*04fd306cSNickeau private FetcherImage $fetchRaster; 4437748cd8SNickeau 4537748cd8SNickeau 4637748cd8SNickeau /** 47*04fd306cSNickeau * @throws ExceptionBadArgument - if the fetcher is not a raster mime and image fetcher 4837748cd8SNickeau */ 49*04fd306cSNickeau public function __construct(MediaMarkup $mediaMarkup) 5037748cd8SNickeau { 51*04fd306cSNickeau $fetcher = $mediaMarkup->getFetcher(); 52*04fd306cSNickeau $mime = $fetcher->getMime(); 53*04fd306cSNickeau if (!$mime->isSupportedRasterImage()) { 54*04fd306cSNickeau throw new ExceptionBadArgument("The mime value ($mime) is not a supported raster image.", self::CANONICAL); 55*04fd306cSNickeau } 56*04fd306cSNickeau if (!($fetcher instanceof FetcherImage)) { 57*04fd306cSNickeau throw new ExceptionBadArgument("The fetcher is not a fetcher image but is a " . get_class($fetcher)); 58*04fd306cSNickeau } 59*04fd306cSNickeau $this->fetchRaster = $fetcher; 60*04fd306cSNickeau 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 69*04fd306cSNickeau * @throws ExceptionNotFound 7037748cd8SNickeau */ 711fa8c418SNickeau public function renderMediaTag(): string 7237748cd8SNickeau { 7337748cd8SNickeau 7437748cd8SNickeau 75*04fd306cSNickeau $fetchRaster = $this->fetchRaster; 76*04fd306cSNickeau 77*04fd306cSNickeau $attributes = $this->mediaMarkup->getExtraMediaTagAttributes() 78*04fd306cSNickeau ->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 * 112*04fd306cSNickeau * Cannot be empty 11337748cd8SNickeau */ 114*04fd306cSNickeau $targetHeight = $fetchRaster->getTargetHeight(); 115*04fd306cSNickeau 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 140*04fd306cSNickeau $imageMargin = SiteConfig::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 14137748cd8SNickeau 14237748cd8SNickeau 14337748cd8SNickeau /** 14437748cd8SNickeau * Srcset and sizes for responsive image 14537748cd8SNickeau * Width is mandatory for responsive image 14637748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 14737748cd8SNickeau */ 148*04fd306cSNickeau 14937748cd8SNickeau 15037748cd8SNickeau /** 1511fa8c418SNickeau * The value of the target image 15237748cd8SNickeau */ 153*04fd306cSNickeau $targetWidth = $fetchRaster->getTargetWidth(); 154*04fd306cSNickeau $fetchRaster->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 15537748cd8SNickeau 15682a60d03SNickeau /** 15782a60d03SNickeau * HTML Width attribute is important to avoid layout shift 15882a60d03SNickeau */ 1594cadd4f8SNickeau $attributes->addOutputAttributeValue("width", $targetWidth . $htmlLengthUnit); 16082a60d03SNickeau /** 16182a60d03SNickeau * We don't allow the image to scale up by default 16282a60d03SNickeau */ 16382a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit); 16482a60d03SNickeau /** 16582a60d03SNickeau * We allow the image to scale down up to 100% of its parent 16682a60d03SNickeau */ 16782a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("width", "100%"); 16882a60d03SNickeau 16937748cd8SNickeau 17037748cd8SNickeau /** 17137748cd8SNickeau * Continue 17237748cd8SNickeau */ 17337748cd8SNickeau $srcSet = ""; 17437748cd8SNickeau $sizes = ""; 17537748cd8SNickeau 17637748cd8SNickeau /** 177*04fd306cSNickeau * Width 178*04fd306cSNickeau * 179*04fd306cSNickeau * We create a series of URL 180*04fd306cSNickeau * for different width and let the browser 181*04fd306cSNickeau * download the best one for: 182*04fd306cSNickeau * * the actual container width 183*04fd306cSNickeau * * the actual of screen resolution 184*04fd306cSNickeau * * and the connection speed. 185*04fd306cSNickeau * 186*04fd306cSNickeau * The max-width value is set 18737748cd8SNickeau */ 188*04fd306cSNickeau $srcValue = $fetchRaster->getFetchUrl(); 189*04fd306cSNickeau /** 190*04fd306cSNickeau * Add samller breakpoints sizes 191*04fd306cSNickeau */ 192*04fd306cSNickeau $intrinsicWidth = $fetchRaster->getIntrinsicWidth(); 193*04fd306cSNickeau foreach (Breakpoint::getBreakpoints() as $breakpoint) { 19437748cd8SNickeau 195*04fd306cSNickeau try { 196*04fd306cSNickeau $breakpointPixels = $breakpoint->getWidth(); 197*04fd306cSNickeau } catch (ExceptionInfinite $e) { 198*04fd306cSNickeau continue; 199*04fd306cSNickeau } 200*04fd306cSNickeau 201*04fd306cSNickeau if ($breakpointPixels > $targetWidth) { 202*04fd306cSNickeau continue; 203*04fd306cSNickeau } 204*04fd306cSNickeau 205*04fd306cSNickeau if ($breakpointPixels > $intrinsicWidth) { 206*04fd306cSNickeau continue; 207*04fd306cSNickeau } 20837748cd8SNickeau 20937748cd8SNickeau if (!empty($srcSet)) { 21037748cd8SNickeau $srcSet .= ", "; 21137748cd8SNickeau $sizes .= ", "; 21237748cd8SNickeau } 213*04fd306cSNickeau $breakpointWidthMinusMargin = $breakpointPixels - $imageMargin; 2144cadd4f8SNickeau 21537748cd8SNickeau 216*04fd306cSNickeau $breakpointRaster = clone $fetchRaster; 217*04fd306cSNickeau if ( 218*04fd306cSNickeau !$fetchRaster->hasHeightRequested() // breakpoint url needs only the h attribute in this case 219*04fd306cSNickeau || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory 220*04fd306cSNickeau ) { 221*04fd306cSNickeau $breakpointRaster->setRequestedWidth($breakpointWidthMinusMargin); 22237748cd8SNickeau } 223*04fd306cSNickeau if ($fetchRaster->hasHeightRequested() // if this is a height request 224*04fd306cSNickeau || $fetchRaster->hasAspectRatioRequested() // width and height are mandatory 225*04fd306cSNickeau ) { 226*04fd306cSNickeau $breakPointHeight = FetcherRaster::round($breakpointWidthMinusMargin / $fetchRaster->getTargetAspectRatio()); 227*04fd306cSNickeau $breakpointRaster->setRequestedHeight($breakPointHeight); 228*04fd306cSNickeau } 229*04fd306cSNickeau 230*04fd306cSNickeau $breakpointUrl = $breakpointRaster->getFetchUrl()->toString(); 231*04fd306cSNickeau 232*04fd306cSNickeau 233*04fd306cSNickeau $srcSet .= "$breakpointUrl {$breakpointWidthMinusMargin}w"; 234*04fd306cSNickeau $sizes .= $this->getSizes($breakpointPixels, $breakpointWidthMinusMargin); 235*04fd306cSNickeau 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*04fd306cSNickeau $srcUrl = $fetchRaster->getFetchUrl()->toString(); 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 /** 2584cadd4f8SNickeau * Html Lazy loading 2594cadd4f8SNickeau */ 260*04fd306cSNickeau $lazyLoadMethod = $this->mediaMarkup->getLazyLoadMethodOrDefault(); 2614cadd4f8SNickeau switch ($lazyLoadMethod) { 262*04fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_HTML_VALUE: 263*04fd306cSNickeau default: 2644cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 2654cadd4f8SNickeau if (!empty($srcSet)) { 2664cadd4f8SNickeau // it the image is small, no srcset for instance 2674cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 2684cadd4f8SNickeau } 2694cadd4f8SNickeau $attributes->addOutputAttributeValue("loading", "lazy"); 2704cadd4f8SNickeau break; 271*04fd306cSNickeau case LazyLoad::LAZY_LOAD_METHOD_LOZAD_VALUE: 2724cadd4f8SNickeau /** 27337748cd8SNickeau * Snippet Lazy loading 27437748cd8SNickeau */ 27537748cd8SNickeau LazyLoad::addLozadSnippet(); 276*04fd306cSNickeau PluginUtility::getSnippetManager()->attachJavascriptFromComponentId("lozad-raster"); 277*04fd306cSNickeau $attributes->addClassName(self::getLazyClass()); 278*04fd306cSNickeau $attributes->addClassName(LazyLoad::getLazyClass()); 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 */ 2934cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 2944cadd4f8SNickeau $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 */ 3014cadd4f8SNickeau $attributes->addOutputAttributeValue("data-sizes", $sizes); 3024cadd4f8SNickeau $attributes->addOutputAttributeValue("data-srcset", $srcSet); 30337748cd8SNickeau 30437748cd8SNickeau } else { 30537748cd8SNickeau 30637748cd8SNickeau /** 30737748cd8SNickeau * Small image but there is no little improvement 30837748cd8SNickeau */ 3094cadd4f8SNickeau $attributes->addOutputAttributeValue("data-src", $srcValue); 3104cadd4f8SNickeau $attributes->addOutputAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 31137748cd8SNickeau 31237748cd8SNickeau } 3131fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 3144cadd4f8SNickeau break; 3154cadd4f8SNickeau } 31637748cd8SNickeau 31737748cd8SNickeau 31837748cd8SNickeau } else { 31937748cd8SNickeau 32037748cd8SNickeau if (!empty($srcSet)) { 3214cadd4f8SNickeau $attributes->addOutputAttributeValue("srcset", $srcSet); 3224cadd4f8SNickeau $attributes->addOutputAttributeValue("sizes", $sizes); 32337748cd8SNickeau } else { 3244cadd4f8SNickeau $attributes->addOutputAttributeValue("src", $srcValue); 32537748cd8SNickeau } 32637748cd8SNickeau 32737748cd8SNickeau } 32837748cd8SNickeau 32937748cd8SNickeau 33037748cd8SNickeau /** 33137748cd8SNickeau * Title (ie alt) 33237748cd8SNickeau */ 333*04fd306cSNickeau $attributes->addOutputAttributeValueIfNotEmpty("alt", $this->getAltNotEmpty()); 334c3437056SNickeau 335c3437056SNickeau /** 33637748cd8SNickeau * Create the img element 33737748cd8SNickeau */ 3381fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 33937748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 34037748cd8SNickeau 34137748cd8SNickeau 342*04fd306cSNickeau return $this->wrapMediaMarkupWithLink($imgHTML); 34337748cd8SNickeau } 34437748cd8SNickeau 34537748cd8SNickeau 346*04fd306cSNickeau public function getLazyLoad(): bool 34737748cd8SNickeau { 348*04fd306cSNickeau 349*04fd306cSNickeau if ($this->mediaMarkup->isLazy() === false) { 350*04fd306cSNickeau return false; 35137748cd8SNickeau } 352*04fd306cSNickeau return SiteConfig::getConfValue(LazyLoad::CONF_RASTER_ENABLE, LazyLoad::CONF_RASTER_ENABLE_DEFAULT); 353*04fd306cSNickeau 35437748cd8SNickeau } 35537748cd8SNickeau 35637748cd8SNickeau /** 35737748cd8SNickeau * @param $screenWidth 35837748cd8SNickeau * @param $imageWidth 35937748cd8SNickeau * @return string sizes with a dpi correction if 36037748cd8SNickeau */ 36137748cd8SNickeau private 3621fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 36337748cd8SNickeau { 36437748cd8SNickeau 36537748cd8SNickeau if ($this->getWithDpiCorrection()) { 36637748cd8SNickeau $dpiBase = 96; 36737748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 36837748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 36937748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 37037748cd8SNickeau } else { 37137748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 37237748cd8SNickeau } 37337748cd8SNickeau return $sizes; 37437748cd8SNickeau } 37537748cd8SNickeau 37637748cd8SNickeau /** 37737748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 37837748cd8SNickeau * 37937748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 38037748cd8SNickeau * 38137748cd8SNickeau * This can be disturbing when debugging responsive sizing image 38237748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 38337748cd8SNickeau * 38437748cd8SNickeau * @return bool 38537748cd8SNickeau */ 38637748cd8SNickeau private 3871fa8c418SNickeau function getWithDpiCorrection(): bool 38837748cd8SNickeau { 38937748cd8SNickeau /** 39037748cd8SNickeau * Support for retina means no DPI correction 39137748cd8SNickeau */ 392*04fd306cSNickeau $retinaEnabled = SiteConfig::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 39337748cd8SNickeau return !$retinaEnabled; 39437748cd8SNickeau } 39537748cd8SNickeau 396*04fd306cSNickeau /** 397*04fd306cSNickeau * Used to select the raster image lazy loaded 398*04fd306cSNickeau * @return string 399*04fd306cSNickeau */ 400*04fd306cSNickeau public static function getLazyClass() 401*04fd306cSNickeau { 402*04fd306cSNickeau return StyleAttribute::addComboStrapSuffix("lazy-raster"); 403*04fd306cSNickeau } 404*04fd306cSNickeau 40537748cd8SNickeau 40637748cd8SNickeau} 407