* @copyright Copyright (c) 2024 Samuel Marshall / JCH Optimize * @license GNU/GPLv3, or later. See LICENSE file * * If LICENSE file missing, see . */ namespace JchOptimize\Core; use _JchOptimizeVendor\V91\GuzzleHttp\Psr7\UriResolver; use _JchOptimizeVendor\V91\Laminas\EventManager\Event; use _JchOptimizeVendor\V91\Psr\Http\Message\UriInterface; use JchOptimize\Core\Exception\PregErrorException; use JchOptimize\Core\Exception\RuntimeException; use JchOptimize\Core\Html\Elements\Img; use JchOptimize\Core\Html\HtmlElementBuilder; use JchOptimize\Core\Html\HtmlManager; use JchOptimize\Core\Platform\PathsInterface; use JchOptimize\Core\SystemUri\SystemUri; use JchOptimize\Core\Uri\AttributeClassifier; use JchOptimize\Core\Uri\OriginClassifier; use JchOptimize\Core\Uri\UriConverter; use JchOptimize\Core\Uri\Utils; class ImageAttributes { private ?Img $currentImgObj = null; private array $imageSize = []; private bool $isImageAttrEnabled; public function __construct( Registry $params, private OriginClassifier $classifier, private PathsInterface $pathsUtils ) { $this->isImageAttrEnabled = (bool)$params->get('img_attributes_enable', '0'); } public function getImageAttributes(array $images): array { $imageAttributes = []; foreach ($images[0] as $image) { if ($this->validateImage($image) === false) { $imageAttributes[] = $image; continue; } $imageAttributes[] = $this->addWidthHeightAttributes(); $this->currentImgObj = null; $this->imageSize = []; } return $imageAttributes; } private function validateImage(mixed $image): bool { try { $imgObj = HtmlElementBuilder::load($image); } catch (PregErrorException) { return false; } if ($imgObj instanceof Img) { $this->currentImgObj = $imgObj; } $uri = $this->getUriFromImageObj($this->currentImgObj); if (!$uri instanceof UriInterface) { return false; } $uri = UriResolver::resolve(SystemUri::currentUri(), $uri); if (!$this->classifier->isInternal($uri)) { return false; } $imagePath = UriConverter::uriToFilePath($uri, $this->pathsUtils); if (!is_file($imagePath)) { return false; } $imageSize = getimagesize($imagePath); if (empty($imageSize) || !is_array($imageSize) || $imageSize[0] <= 1 || $imageSize[1] <= 1) { return false; } $this->imageSize = $imageSize; return true; } private function getUriFromImageObj(Img $imgObj): UriInterface|string|false { if ($imgObj->hasAttribute('src') && ($src = $imgObj->getSrc()) instanceof UriInterface) { return $src; } elseif ($imgObj->hasAttribute('data-src')) { $dataSrc = $imgObj->attributeValue('data-src'); if (AttributeClassifier::isUriCandidate($dataSrc)) { return Utils::uriFor($dataSrc); } } return false; } private function addWidthHeightAttributes(): string { if ($this->currentImgObj->hasAttribute('width')) { try { return $this->addHeightAttributeFromWidth(); } catch (RuntimeException $e) { } } if ($this->currentImgObj->hasAttribute('height')) { try { return $this->addWidthAttributeFromHeight(); } catch (RuntimeException $e) { } } if ($this->isImageAttrEnabled) { $this->currentImgObj->width($this->imageSize[0]); $this->currentImgObj->height($this->imageSize[1]); } else { $this->currentImgObj->data('width', $this->imageSize[0]); $this->currentImgObj->data('height', $this->imageSize[1]); } return $this->currentImgObj->render(); } private function addAttrFromExistingAttr(string $existingAttr): string { $existingAttrValue = $this->currentImgObj->attributeValue($existingAttr); if ($existingAttrValue !== false) { $existingAttrValue = $this->cleanAttrValue($existingAttrValue, $existingAttr); if ($existingAttrValue) { return $this->addOtherAttrByAspectRatio($existingAttrValue, $existingAttr); } } throw new RuntimeException('Attribute not found'); } private function cleanAttrValue(mixed $value, $existingAttr): string { $cleanedAttrValue = (string)preg_replace('#[^0-9]#', '', $value, -1, $count); if ($count > 0) { $this->currentImgObj->attribute($existingAttr, $cleanedAttrValue); } return $cleanedAttrValue; } private function addHeightAttributeFromWidth(): string { return $this->addAttrFromExistingAttr('width'); } private function addWidthAttributeFromHeight(): string { return $this->addAttrFromExistingAttr('height'); } private function addOtherAttrByAspectRatio(string $existingAttrValue, string $existingAttr): string { if ($existingAttr == 'width') { $height = (string)round( ($this->imageSize[1] / $this->imageSize[0]) * (int)$existingAttrValue ); $this->isImageAttrEnabled ? $this->currentImgObj->height($height) : $this->currentImgObj->data('height', $height); } else { $width = (string)round( ($this->imageSize[0] / $this->imageSize[1]) * (int)$existingAttrValue ); $this->isImageAttrEnabled ? $this->currentImgObj->width($width) : $this->currentImgObj->data('width', $width); } return $this->currentImgObj->render(); } public function loadImageAttributesCss(Event $event): void { if ($this->isImageAttrEnabled) { $style = HtmlElementBuilder::style() ->class('jchoptimize-image-attributes') ->addChild('img{max-width: 100%; height: auto;}'); /** @var HtmlManager $htmlManager */ $htmlManager = $event->getTarget(); $htmlManager->appendChildToHead($style->render()); } } }