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 7037748cd8SNickeau */ 711fa8c418SNickeau public function renderMediaTag(): string 7237748cd8SNickeau { 73c3437056SNickeau /** 74c3437056SNickeau * @var ImageRaster $image 75c3437056SNickeau */ 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 = ""; 111*82a60d03SNickeau $cssLengthUnit = "px"; 11237748cd8SNickeau 11337748cd8SNickeau /** 11437748cd8SNickeau * Height 11537748cd8SNickeau * The logical height that the image should take on the page 11637748cd8SNickeau * 11737748cd8SNickeau * Note: The style is also set in {@link Dimension::processWidthAndHeight()} 11837748cd8SNickeau * 11937748cd8SNickeau */ 120*82a60d03SNickeau try { 1211fa8c418SNickeau $targetHeight = $image->getTargetHeight(); 122*82a60d03SNickeau } catch (ExceptionCombo $e) { 123*82a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The target height reports a problem: {$e->getMessage()}"); 124*82a60d03SNickeau return ""; 125*82a60d03SNickeau } 1261fa8c418SNickeau if (!empty($targetHeight)) { 127*82a60d03SNickeau /** 128*82a60d03SNickeau * HTML height attribute is important for the ratio calculation 129*82a60d03SNickeau * No layout shift 130*82a60d03SNickeau */ 1311fa8c418SNickeau $attributes->addHtmlAttributeValue("height", $targetHeight . $htmlLengthUnit); 132*82a60d03SNickeau /** 133*82a60d03SNickeau * We don't allow the image to scale up by default 134*82a60d03SNickeau */ 135*82a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-height", $targetHeight . $cssLengthUnit); 13637748cd8SNickeau } 13737748cd8SNickeau 13837748cd8SNickeau 13937748cd8SNickeau /** 14037748cd8SNickeau * Width 14137748cd8SNickeau * 14237748cd8SNickeau * We create a series of URL 14337748cd8SNickeau * for different width and let the browser 14437748cd8SNickeau * download the best one for: 14537748cd8SNickeau * * the actual container width 14637748cd8SNickeau * * the actual of screen resolution 14737748cd8SNickeau * * and the connection speed. 14837748cd8SNickeau * 14937748cd8SNickeau * The max-width value is set 15037748cd8SNickeau */ 151*82a60d03SNickeau try { 1521fa8c418SNickeau $mediaWidthValue = $image->getIntrinsicWidth(); 153*82a60d03SNickeau } catch (ExceptionCombo $e) { 154*82a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The intrinsic width reports a problem: {$e->getMessage()}"); 155*82a60d03SNickeau return ""; 156*82a60d03SNickeau } 1571fa8c418SNickeau $srcValue = $image->getUrl(); 15837748cd8SNickeau 15937748cd8SNickeau /** 16037748cd8SNickeau * Responsive image src set building 16137748cd8SNickeau * We have chosen 16237748cd8SNickeau * * 375: Iphone6 16337748cd8SNickeau * * 768: Ipad 16437748cd8SNickeau * * 1024: Ipad Pro 16537748cd8SNickeau * 16637748cd8SNickeau */ 16737748cd8SNickeau // The image margin applied 16837748cd8SNickeau $imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px"); 16937748cd8SNickeau 17037748cd8SNickeau 17137748cd8SNickeau /** 17237748cd8SNickeau * Srcset and sizes for responsive image 17337748cd8SNickeau * Width is mandatory for responsive image 17437748cd8SNickeau * Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images 17537748cd8SNickeau */ 17637748cd8SNickeau if (!empty($mediaWidthValue)) { 17737748cd8SNickeau 17837748cd8SNickeau /** 1791fa8c418SNickeau * The value of the target image 18037748cd8SNickeau */ 181*82a60d03SNickeau try { 1821fa8c418SNickeau $targetWidth = $image->getTargetWidth(); 183*82a60d03SNickeau } catch (ExceptionCombo $e) { 184*82a60d03SNickeau LogUtility::msg("No rendering for the image ($image). The target width reports a problem: {$e->getMessage()}"); 185*82a60d03SNickeau return ""; 186*82a60d03SNickeau } 1871fa8c418SNickeau if (!empty($targetWidth)) { 18837748cd8SNickeau 1891fa8c418SNickeau if (!empty($targetHeight)) { 1901fa8c418SNickeau $image->checkLogicalRatioAgainstTargetRatio($targetWidth, $targetHeight); 19137748cd8SNickeau } 192*82a60d03SNickeau /** 193*82a60d03SNickeau * HTML Width attribute is important to avoid layout shift 194*82a60d03SNickeau */ 1951fa8c418SNickeau $attributes->addHtmlAttributeValue("width", $targetWidth . $htmlLengthUnit); 196*82a60d03SNickeau /** 197*82a60d03SNickeau * We don't allow the image to scale up by default 198*82a60d03SNickeau */ 199*82a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("max-width", $targetWidth . $cssLengthUnit); 200*82a60d03SNickeau /** 201*82a60d03SNickeau * We allow the image to scale down up to 100% of its parent 202*82a60d03SNickeau */ 203*82a60d03SNickeau $attributes->addStyleDeclarationIfNotSet("width", "100%"); 204*82a60d03SNickeau 20537748cd8SNickeau } 20637748cd8SNickeau 20737748cd8SNickeau /** 20837748cd8SNickeau * Continue 20937748cd8SNickeau */ 21037748cd8SNickeau $srcSet = ""; 21137748cd8SNickeau $sizes = ""; 21237748cd8SNickeau 21337748cd8SNickeau /** 21437748cd8SNickeau * Add smaller sizes 21537748cd8SNickeau */ 21637748cd8SNickeau foreach (self::BREAKPOINTS as $breakpointWidth) { 21737748cd8SNickeau 2181fa8c418SNickeau if ($targetWidth > $breakpointWidth) { 21937748cd8SNickeau 22037748cd8SNickeau if (!empty($srcSet)) { 22137748cd8SNickeau $srcSet .= ", "; 22237748cd8SNickeau $sizes .= ", "; 22337748cd8SNickeau } 22437748cd8SNickeau $breakpointWidthMinusMargin = $breakpointWidth - $imageMargin; 225c3437056SNickeau $xsmUrl = $image->getUrlForSrcSetAtBreakpoint($breakpointWidthMinusMargin); 22637748cd8SNickeau $srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w"; 22737748cd8SNickeau $sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin); 22837748cd8SNickeau 22937748cd8SNickeau } 23037748cd8SNickeau 23137748cd8SNickeau } 23237748cd8SNickeau 23337748cd8SNickeau /** 23437748cd8SNickeau * Add the last size 2351fa8c418SNickeau * If the target image is really small, srcset and sizes are empty 23637748cd8SNickeau */ 23737748cd8SNickeau if (!empty($srcSet)) { 23837748cd8SNickeau $srcSet .= ", "; 23937748cd8SNickeau $sizes .= ", "; 240c3437056SNickeau $srcUrl = $image->getUrlForSrcSetAtBreakpoint($targetWidth); 2411fa8c418SNickeau $srcSet .= "$srcUrl {$targetWidth}w"; 2421fa8c418SNickeau $sizes .= "{$targetWidth}px"; 24337748cd8SNickeau } 24437748cd8SNickeau 24537748cd8SNickeau /** 24637748cd8SNickeau * Lazy load 24737748cd8SNickeau */ 24837748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 24937748cd8SNickeau if ($lazyLoad) { 25037748cd8SNickeau 25137748cd8SNickeau /** 25237748cd8SNickeau * Snippet Lazy loading 25337748cd8SNickeau */ 25437748cd8SNickeau LazyLoad::addLozadSnippet(); 25537748cd8SNickeau PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster"); 2561fa8c418SNickeau $attributes->addClassName(self::LAZY_CLASS); 2571fa8c418SNickeau $attributes->addClassName(LazyLoad::LAZY_CLASS); 25837748cd8SNickeau 25937748cd8SNickeau /** 26037748cd8SNickeau * A small image has no srcset 26137748cd8SNickeau * 26237748cd8SNickeau */ 26337748cd8SNickeau if (!empty($srcSet)) { 26437748cd8SNickeau 26537748cd8SNickeau /** 26637748cd8SNickeau * !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!! 26737748cd8SNickeau * https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern 26837748cd8SNickeau * The transparent image has a fix dimension aspect ratio of 1x1 making 26937748cd8SNickeau * a bad reserved space for the image 27037748cd8SNickeau * We use a svg instead 27137748cd8SNickeau */ 2721fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 2731fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 27437748cd8SNickeau /** 27537748cd8SNickeau * We use `data-sizes` and not `sizes` 27637748cd8SNickeau * because `sizes` without `srcset` 27737748cd8SNickeau * shows the broken image symbol 27837748cd8SNickeau * Javascript changes them at the same time 27937748cd8SNickeau */ 2801fa8c418SNickeau $attributes->addHtmlAttributeValue("data-sizes", $sizes); 2811fa8c418SNickeau $attributes->addHtmlAttributeValue("data-srcset", $srcSet); 28237748cd8SNickeau 28337748cd8SNickeau } else { 28437748cd8SNickeau 28537748cd8SNickeau /** 28637748cd8SNickeau * Small image but there is no little improvement 28737748cd8SNickeau */ 2881fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 289c3437056SNickeau $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder($targetWidth, $targetHeight)); 29037748cd8SNickeau 29137748cd8SNickeau } 29237748cd8SNickeau 2931fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 29437748cd8SNickeau 29537748cd8SNickeau 29637748cd8SNickeau } else { 29737748cd8SNickeau 29837748cd8SNickeau if (!empty($srcSet)) { 2991fa8c418SNickeau $attributes->addHtmlAttributeValue("srcset", $srcSet); 3001fa8c418SNickeau $attributes->addHtmlAttributeValue("sizes", $sizes); 30137748cd8SNickeau } else { 3021fa8c418SNickeau $attributes->addHtmlAttributeValue("src", $srcValue); 30337748cd8SNickeau } 30437748cd8SNickeau 30537748cd8SNickeau } 30637748cd8SNickeau 30737748cd8SNickeau } else { 30837748cd8SNickeau 30937748cd8SNickeau // No width, no responsive possibility 31037748cd8SNickeau $lazyLoad = $this->getLazyLoad(); 31137748cd8SNickeau if ($lazyLoad) { 31237748cd8SNickeau 3131fa8c418SNickeau LazyLoad::addPlaceholderBackground($attributes); 3141fa8c418SNickeau $attributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder()); 3151fa8c418SNickeau $attributes->addHtmlAttributeValue("data-src", $srcValue); 31637748cd8SNickeau 31737748cd8SNickeau } 31837748cd8SNickeau 31937748cd8SNickeau } 32037748cd8SNickeau 32137748cd8SNickeau 32237748cd8SNickeau /** 32337748cd8SNickeau * Title (ie alt) 32437748cd8SNickeau */ 3251fa8c418SNickeau $attributes->addHtmlAttributeValueIfNotEmpty("alt", $image->getAltNotEmpty()); 3261fa8c418SNickeau 3271fa8c418SNickeau /** 3281fa8c418SNickeau * TODO: Side effect of the fact that we use the same attributes 3291fa8c418SNickeau * Title attribute of a media is the alt of an image 3301fa8c418SNickeau * And title should not be in an image tag 3311fa8c418SNickeau */ 3321fa8c418SNickeau $attributes->removeAttributeIfPresent(TagAttributes::TITLE_KEY); 33337748cd8SNickeau 33437748cd8SNickeau /** 335c3437056SNickeau * Old model where the src is parsed and the path 336c3437056SNickeau * is in the attributes 337c3437056SNickeau */ 338c3437056SNickeau $attributes->removeAttributeIfPresent(PagePath::PROPERTY_NAME); 339c3437056SNickeau 340c3437056SNickeau /** 34137748cd8SNickeau * Create the img element 34237748cd8SNickeau */ 3431fa8c418SNickeau $htmlAttributes = $attributes->toHTMLAttributeString(); 34437748cd8SNickeau $imgHTML = '<img ' . $htmlAttributes . '/>'; 34537748cd8SNickeau 34637748cd8SNickeau } else { 34737748cd8SNickeau 34837748cd8SNickeau $imgHTML = "<span class=\"text-danger\">The image ($this) does not exist</span>"; 34937748cd8SNickeau 35037748cd8SNickeau } 35137748cd8SNickeau 35237748cd8SNickeau return $imgHTML; 35337748cd8SNickeau } 35437748cd8SNickeau 35537748cd8SNickeau 35637748cd8SNickeau public 35737748cd8SNickeau function getLazyLoad() 35837748cd8SNickeau { 35937748cd8SNickeau $lazyLoad = parent::getLazyLoad(); 36037748cd8SNickeau if ($lazyLoad !== null) { 36137748cd8SNickeau return $lazyLoad; 36237748cd8SNickeau } else { 363c3437056SNickeau return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE, RasterImageLink::CONF_LAZY_LOADING_ENABLE_DEFAULT); 36437748cd8SNickeau } 36537748cd8SNickeau } 36637748cd8SNickeau 36737748cd8SNickeau /** 36837748cd8SNickeau * @param $screenWidth 36937748cd8SNickeau * @param $imageWidth 37037748cd8SNickeau * @return string sizes with a dpi correction if 37137748cd8SNickeau */ 37237748cd8SNickeau private 3731fa8c418SNickeau function getSizes($screenWidth, $imageWidth): string 37437748cd8SNickeau { 37537748cd8SNickeau 37637748cd8SNickeau if ($this->getWithDpiCorrection()) { 37737748cd8SNickeau $dpiBase = 96; 37837748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px"; 37937748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px"; 38037748cd8SNickeau $sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px"; 38137748cd8SNickeau } else { 38237748cd8SNickeau $sizes = "(max-width: {$screenWidth}px) {$imageWidth}px"; 38337748cd8SNickeau } 38437748cd8SNickeau return $sizes; 38537748cd8SNickeau } 38637748cd8SNickeau 38737748cd8SNickeau /** 38837748cd8SNickeau * Return if the DPI correction is enabled or not for responsive image 38937748cd8SNickeau * 39037748cd8SNickeau * Mobile have a higher DPI and can then fit a bigger image on a smaller size. 39137748cd8SNickeau * 39237748cd8SNickeau * This can be disturbing when debugging responsive sizing image 39337748cd8SNickeau * If you want also to use less bandwidth, this is also useful. 39437748cd8SNickeau * 39537748cd8SNickeau * @return bool 39637748cd8SNickeau */ 39737748cd8SNickeau private 3981fa8c418SNickeau function getWithDpiCorrection(): bool 39937748cd8SNickeau { 40037748cd8SNickeau /** 40137748cd8SNickeau * Support for retina means no DPI correction 40237748cd8SNickeau */ 40337748cd8SNickeau $retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0); 40437748cd8SNickeau return !$retinaEnabled; 40537748cd8SNickeau } 40637748cd8SNickeau 40737748cd8SNickeau 40837748cd8SNickeau} 409