*
*/
namespace ComboStrap;
require_once(__DIR__ . '/MediaLink.php');
require_once(__DIR__ . '/LazyLoad.php');
require_once(__DIR__ . '/PluginUtility.php');
/**
* Image
* This is the class that handles the
* raster image type of the dokuwiki {@link MediaLink}
*
* The real documentation can be found on the image page
* @link https://www.dokuwiki.org/images
*
* Doc:
* https://web.dev/optimize-cls/#images-without-dimensions
* https://web.dev/cls/
*/
class RasterImageLink extends MediaLink
{
const CANONICAL = "raster";
const CONF_LAZY_LOADING_ENABLE = "rasterImageLazyLoadingEnable";
const RESPONSIVE_CLASS = "img-fluid";
const CONF_RESPONSIVE_IMAGE_MARGIN = "responsiveImageMargin";
const CONF_RETINA_SUPPORT_ENABLED = "retinaRasterImageEnable";
const LAZY_CLASS = "lazy-raster-combo";
const BREAKPOINTS =
array(
"xs" => 375,
"sm" => 576,
"md" => 768,
"lg" => 992
);
private $imageWidth = null;
/**
* @var int
*/
private $imageWeight = null;
/**
* See {@link image_type_to_mime_type}
* @var int
*/
private $imageType;
private $wasAnalyzed = false;
/**
* @var bool
*/
private $analyzable = false;
/**
* @var mixed - the mime from the {@link RasterImageLink::analyzeImageIfNeeded()}
*/
private $mime;
/**
* RasterImageLink constructor.
* @param $ref
* @param TagAttributes $tagAttributes
*/
public function __construct($ref, $tagAttributes = null)
{
parent::__construct($ref, $tagAttributes);
$this->getTagAttributes()->setLogicalTag(self::CANONICAL);
}
/**
* @param string $ampersand
* @param null $localWidth - the asked width - use for responsive image
* @return string|null
*/
public function getUrl($ampersand = DokuwikiUrl::URL_ENCODED_AND, $localWidth = null)
{
if ($this->exists()) {
/**
* Link attribute
*/
$att = array();
// Width is driving the computation
if ($localWidth != null && $localWidth != $this->getMediaWidth()) {
$att['w'] = $localWidth;
// Height
$height = $this->getImgTagHeightValue($localWidth);
if (!empty($height)) {
$att['h'] = $height;
$this->checkWidthAndHeightRatioAndReturnTheGoodValue($localWidth, $height);
}
}
if ($this->getCache()) {
$att[CacheMedia::CACHE_KEY] = $this->getCache();
}
$direct = true;
return ml($this->getId(), $att, $direct, $ampersand, true);
} else {
return false;
}
}
public function getAbsoluteUrl()
{
return $this->getUrl();
}
/**
* Render a link
* Snippet derived from {@link \Doku_Renderer_xhtml::internalmedia()}
* A media can be a video also (Use
* @return string
*/
public function renderMediaTag()
{
if ($this->exists()) {
/**
* No dokuwiki type attribute
*/
$this->tagAttributes->removeComponentAttributeIfPresent(MediaLink::MEDIA_DOKUWIKI_TYPE);
$this->tagAttributes->removeComponentAttributeIfPresent(MediaLink::DOKUWIKI_SRC);
/**
* Responsive image
* https://getbootstrap.com/docs/5.0/content/images/
* to apply max-width: 100%; and height: auto;
*
* Even if the resizing is requested by height,
* the height: auto on styling is needed to conserve the ratio
* while scaling down the screen
*/
$this->tagAttributes->addClassName(self::RESPONSIVE_CLASS);
/**
* width and height to give the dimension ratio
* They have an effect on the space reservation
* but not on responsive image at all
* To allow responsive height, the height style property is set at auto
* (ie img-fluid in bootstrap)
*/
// The unit is not mandatory in HTML, this is expected to be CSS pixel
// https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
// The HTML validator does not expect an unit otherwise it send an error
// https://validator.w3.org/
$htmlLengthUnit = "";
/**
* Height
* The logical height that the image should take on the page
*
* Note: The style is also set in {@link Dimension::processWidthAndHeight()}
*
* The doc is {@link https://www.dokuwiki.org/images#resizing}
* See the ''0x20''
*/
$imgTagHeight = $this->getImgTagHeightValue();
if (!empty($imgTagHeight)) {
$this->tagAttributes->addHtmlAttributeValue("height", $imgTagHeight . $htmlLengthUnit);
}
/**
* Width
*
* We create a series of URL
* for different width and let the browser
* download the best one for:
* * the actual container width
* * the actual of screen resolution
* * and the connection speed.
*
* The max-width value is set
*/
$mediaWidthValue = $this->getMediaWidth();
$srcValue = $this->getUrl();
/**
* Responsive image src set building
* We have chosen
* * 375: Iphone6
* * 768: Ipad
* * 1024: Ipad Pro
*
*/
// The image margin applied
$imageMargin = PluginUtility::getConfValue(self::CONF_RESPONSIVE_IMAGE_MARGIN, "20px");
/**
* Srcset and sizes for responsive image
* Width is mandatory for responsive image
* Ref https://developers.google.com/search/docs/advanced/guidelines/google-images#responsive-images
*/
if (!empty($mediaWidthValue)) {
/**
* The internal intrinsic value of the image
*/
$imgTagWidth = $this->getImgTagWidthValue();
if (!empty($imgTagWidth)) {
if (!empty($imgTagHeight)) {
$imgTagWidth = $this->checkWidthAndHeightRatioAndReturnTheGoodValue($imgTagWidth, $imgTagHeight);
}
$this->tagAttributes->addHtmlAttributeValue("width", $imgTagWidth . $htmlLengthUnit);
}
/**
* Continue
*/
$srcSet = "";
$sizes = "";
/**
* Add smaller sizes
*/
foreach (self::BREAKPOINTS as $breakpointWidth) {
if ($imgTagWidth > $breakpointWidth) {
if (!empty($srcSet)) {
$srcSet .= ", ";
$sizes .= ", ";
}
$breakpointWidthMinusMargin = $breakpointWidth - $imageMargin;
$xsmUrl = $this->getUrl(DokuwikiUrl::URL_ENCODED_AND, $breakpointWidthMinusMargin);
$srcSet .= "$xsmUrl {$breakpointWidthMinusMargin}w";
$sizes .= $this->getSizes($breakpointWidth, $breakpointWidthMinusMargin);
}
}
/**
* Add the last size
* If the image is really small, srcset and sizes are empty
*/
if (!empty($srcSet)) {
$srcSet .= ", ";
$sizes .= ", ";
$srcUrl = $this->getUrl(DokuwikiUrl::URL_ENCODED_AND, $imgTagWidth);
$srcSet .= "$srcUrl {$imgTagWidth}w";
$sizes .= "{$imgTagWidth}px";
}
/**
* Lazy load
*/
$lazyLoad = $this->getLazyLoad();
if ($lazyLoad) {
/**
* Snippet Lazy loading
*/
LazyLoad::addLozadSnippet();
PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar("lozad-raster");
$this->tagAttributes->addClassName(self::LAZY_CLASS);
$this->tagAttributes->addClassName(LazyLoad::LAZY_CLASS);
/**
* A small image has no srcset
*
*/
if (!empty($srcSet)) {
/**
* !!!!! DON'T FOLLOW THIS ADVICE !!!!!!!!!
* https://github.com/aFarkas/lazysizes/#modern-transparent-srcset-pattern
* The transparent image has a fix dimension aspect ratio of 1x1 making
* a bad reserved space for the image
* We use a svg instead
*/
$this->tagAttributes->addHtmlAttributeValue("src", $srcValue);
$this->tagAttributes->addHtmlAttributeValue("srcset", LazyLoad::getPlaceholder($imgTagWidth,$imgTagHeight));
/**
* We use `data-sizes` and not `sizes`
* because `sizes` without `srcset`
* shows the broken image symbol
* Javascript changes them at the same time
*/
$this->tagAttributes->addHtmlAttributeValue("data-sizes", $sizes);
$this->tagAttributes->addHtmlAttributeValue("data-srcset", $srcSet);
} else {
/**
* Small image but there is no little improvement
*/
$this->tagAttributes->addHtmlAttributeValue("data-src", $srcValue);
}
LazyLoad::addPlaceholderBackground($this->tagAttributes);
} else {
if (!empty($srcSet)) {
$this->tagAttributes->addHtmlAttributeValue("srcset", $srcSet);
$this->tagAttributes->addHtmlAttributeValue("sizes", $sizes);
} else {
$this->tagAttributes->addHtmlAttributeValue("src", $srcValue);
}
}
} else {
// No width, no responsive possibility
$lazyLoad = $this->getLazyLoad();
if ($lazyLoad) {
LazyLoad::addPlaceholderBackground($this->tagAttributes);
$this->tagAttributes->addHtmlAttributeValue("src", LazyLoad::getPlaceholder());
$this->tagAttributes->addHtmlAttributeValue("data-src", $srcValue);
}
}
/**
* Title (ie alt)
*/
if ($this->tagAttributes->hasComponentAttribute(TagAttributes::TITLE_KEY)) {
$title = $this->tagAttributes->getValueAndRemove(TagAttributes::TITLE_KEY);
$this->tagAttributes->addHtmlAttributeValueIfNotEmpty("alt", $title);
}
/**
* Create the img element
*/
$htmlAttributes = $this->tagAttributes->toHTMLAttributeString();
$imgHTML = '
';
} else {
$imgHTML = "The image ($this) does not exist";
}
return $imgHTML;
}
/**
* @return int - the width of the image from the file
*/
public
function getMediaWidth()
{
$this->analyzeImageIfNeeded();
return $this->imageWidth;
}
/**
* @return int - the height of the image from the file
*/
public
function getMediaHeight()
{
$this->analyzeImageIfNeeded();
return $this->imageWeight;
}
private
function analyzeImageIfNeeded()
{
if (!$this->wasAnalyzed) {
if ($this->exists()) {
/**
* Based on {@link media_image_preview_size()}
* $dimensions = media_image_preview_size($this->id, '', false);
*/
$imageInfo = array();
$imageSize = getimagesize($this->getFileSystemPath(), $imageInfo);
if ($imageSize === false) {
$this->analyzable = false;
LogUtility::msg("We couldn't retrieve the type and dimensions of the image ($this). The image format seems to be not supported.", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
} else {
$this->analyzable = true;
$this->imageWidth = (int)$imageSize[0];
if (empty($this->imageWidth)) {
$this->analyzable = false;
}
$this->imageWeight = (int)$imageSize[1];
if (empty($this->imageWeight)) {
$this->analyzable = false;
}
$this->imageType = (int)$imageSize[2];
$this->mime = $imageSize[3];
}
}
}
$this->wasAnalyzed = true;
}
/**
*
* @return bool true if we could extract the dimensions
*/
public
function isAnalyzable()
{
$this->analyzeImageIfNeeded();
return $this->analyzable;
}
public function getRequestedHeight()
{
$requestedHeight = parent::getRequestedHeight();
if (!empty($requestedHeight)) {
// it should not be bigger than the media Height
$mediaHeight = $this->getMediaHeight();
if (!empty($mediaHeight)) {
if ($requestedHeight > $mediaHeight) {
LogUtility::msg("For the image ($this), the requested height of ($requestedHeight) can not be bigger than the intrinsic height of ($mediaHeight). The height was then set to its natural height ($mediaHeight)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
$requestedHeight = $mediaHeight;
}
}
}
return $requestedHeight;
}
public function getRequestedWidth()
{
$requestedWidth = parent::getRequestedWidth();
if (!empty($requestedWidth)) {
// it should not be bigger than the media Height
$mediaWidth = $this->getMediaWidth();
if (!empty($mediaWidth)) {
if ($requestedWidth > $mediaWidth) {
global $ID;
if ($ID != "wiki:syntax") {
// There is a bug in the wiki syntax page
// {{wiki:dokuwiki-128.png?200x50}}
// https://forum.dokuwiki.org/d/19313-bugtypo-how-to-make-a-request-to-change-the-syntax-page-on-dokuwikii
LogUtility::msg("For the image ($this), the requested width of ($requestedWidth) can not be bigger than the intrinsic width of ($mediaWidth). The width was then set to its natural width ($mediaWidth)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
}
$requestedWidth = $mediaWidth;
}
}
}
return $requestedWidth;
}
public
function getLazyLoad()
{
$lazyLoad = parent::getLazyLoad();
if ($lazyLoad !== null) {
return $lazyLoad;
} else {
return PluginUtility::getConfValue(RasterImageLink::CONF_LAZY_LOADING_ENABLE);
}
}
/**
* @param $screenWidth
* @param $imageWidth
* @return string sizes with a dpi correction if
*/
private
function getSizes($screenWidth, $imageWidth)
{
if ($this->getWithDpiCorrection()) {
$dpiBase = 96;
$sizes = "(max-width: {$screenWidth}px) and (min-resolution:" . (3 * $dpiBase) . "dpi) " . intval($imageWidth / 3) . "px";
$sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (2 * $dpiBase) . "dpi) " . intval($imageWidth / 2) . "px";
$sizes .= ", (max-width: {$screenWidth}px) and (min-resolution:" . (1 * $dpiBase) . "dpi) {$imageWidth}px";
} else {
$sizes = "(max-width: {$screenWidth}px) {$imageWidth}px";
}
return $sizes;
}
/**
* Return if the DPI correction is enabled or not for responsive image
*
* Mobile have a higher DPI and can then fit a bigger image on a smaller size.
*
* This can be disturbing when debugging responsive sizing image
* If you want also to use less bandwidth, this is also useful.
*
* @return bool
*/
private
function getWithDpiCorrection()
{
/**
* Support for retina means no DPI correction
*/
$retinaEnabled = PluginUtility::getConfValue(self::CONF_RETINA_SUPPORT_ENABLED, 0);
return !$retinaEnabled;
}
}