diff --git a/Classes/Domain/AbstractImageSource.php b/Classes/Domain/AbstractImageSource.php index 81d2b36..026178b 100644 --- a/Classes/Domain/AbstractImageSource.php +++ b/Classes/Domain/AbstractImageSource.php @@ -7,7 +7,6 @@ use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; -use Neos\Utility\Arrays; use Psr\Log\LoggerInterface; use Sitegeist\Kaleidoscope\EelHelpers\ImageSourceHelperInterface; @@ -197,39 +196,15 @@ public function withVariantPreset(string $presetIdentifier, string $presetVarian } /** - * Render sourceset Attribute for various media descriptors. + * Render sourceset Attribute non-scalable media. * * @param mixed $mediaDescriptors + * @param bool $allowUpScaling * * @return string */ - public function srcset($mediaDescriptors): string + public function srcset($mediaDescriptors, bool $allowUpScaling = false): string { - if ($this instanceof ScalableImageSourceInterface) { - $srcsetArray = []; - - if (is_array($mediaDescriptors) || $mediaDescriptors instanceof \Traversable) { - $descriptors = $mediaDescriptors; - } else { - $descriptors = Arrays::trimExplode(',', (string) $mediaDescriptors); - } - - foreach ($descriptors as $descriptor) { - if (preg_match('/^(?[0-9]+)w$/u', $descriptor, $matches)) { - $width = (int) $matches['width']; - $scaleFactor = $width / $this->width(); - $scaled = $this->scale($scaleFactor); - $srcsetArray[] = $scaled->src() . ' ' . $width . 'w'; - } elseif (preg_match('/^(?[0-9\\.]+)x$/u', $descriptor, $matches)) { - $factor = (float) $matches['factor']; - $scaled = $this->scale($factor); - $srcsetArray[] = $scaled->src() . ' ' . $factor . 'x'; - } - } - - return implode(', ', $srcsetArray); - } - return $this->src(); } diff --git a/Classes/Domain/AbstractScalableImageSource.php b/Classes/Domain/AbstractScalableImageSource.php index 60deaea..2e8924b 100644 --- a/Classes/Domain/AbstractScalableImageSource.php +++ b/Classes/Domain/AbstractScalableImageSource.php @@ -7,12 +7,14 @@ use Imagine\Image\Box; use Imagine\Image\ImagineInterface; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Media\Domain\Model\Adjustment\CropImageAdjustment; use Neos\Media\Domain\Model\Adjustment\ImageAdjustmentInterface; use Neos\Media\Domain\Model\Adjustment\ResizeImageAdjustment; use Neos\Media\Domain\ValueObject\Configuration\Adjustment; use Neos\Media\Domain\ValueObject\Configuration\VariantPreset; use Neos\Utility\ObjectAccess; +use Neos\Utility\Arrays; use Sitegeist\Kaleidoscope\EelHelpers\ScalableImageSourceHelperInterface; abstract class AbstractScalableImageSource extends AbstractImageSource implements ScalableImageSourceInterface, ScalableImageSourceHelperInterface @@ -34,13 +36,18 @@ abstract class AbstractScalableImageSource extends AbstractImageSource implement */ protected $baseHeight; + /** + * @var bool + */ + protected $allowUpScaling = false; + /** * @param int|null $targetWidth * @param bool $preserveAspect * - * @return ImageSourceInterface + * @return ScalableImageSourceInterface */ - public function withWidth(int $targetWidth = null, bool $preserveAspect = false): ImageSourceInterface + public function withWidth(int $targetWidth = null, bool $preserveAspect = false): ScalableImageSourceInterface { $newSource = clone $this; $newSource->targetWidth = $targetWidth; @@ -60,9 +67,9 @@ public function withWidth(int $targetWidth = null, bool $preserveAspect = false) * @param int|null $targetHeight * @param bool $preserveAspect * - * @return ImageSourceInterface + * @return ScalableImageSourceInterface */ - public function withHeight(int $targetHeight = null, bool $preserveAspect = false): ImageSourceInterface + public function withHeight(int $targetHeight = null, bool $preserveAspect = false): ScalableImageSourceInterface { $newSource = clone $this; $newSource->targetHeight = $targetHeight; @@ -78,14 +85,32 @@ public function withHeight(int $targetHeight = null, bool $preserveAspect = fals return $newSource; } + /** + * @param int $targetWidth + * @param int $targetHeight + * + * @return ScalableImageSourceInterface + */ + public function withDimensions(int $targetWidth, int $targetHeight): ScalableImageSourceInterface + { + $newSource = clone $this; + $newSource->targetWidth = $targetWidth; + $newSource->targetHeight = $targetHeight; + + return $newSource; + } + + /** * @param float $factor + * @param bool $allowUpScaling * - * @return ImageSourceInterface + * @return ScalableImageSourceInterface */ - public function scale(float $factor): ImageSourceInterface + public function scale(float $factor, bool $allowUpScaling = false): ScalableImageSourceInterface { $scaledHelper = clone $this; + $scaledHelper->allowUpScaling = $allowUpScaling; if ($this->targetWidth && $this->targetHeight) { $scaledHelper = $scaledHelper->withDimensions((int) round($factor * $this->targetWidth), (int) round($factor * $this->targetHeight)); @@ -213,4 +238,72 @@ protected function createAdjustment(Adjustment $adjustmentConfiguration): ImageA return $adjustment; } + + /** + * Render srcset Attribute for various media descriptors. + * + * If upscaling is not allowed and the width is greater than the base width, + * use the base width. + * + * @param $mediaDescriptors + * @param bool $allowUpScaling + * + * @return string + */ + public function srcset($mediaDescriptors, bool $allowUpScaling = false): string + { + $srcsetArray = []; + + if (is_array($mediaDescriptors) || $mediaDescriptors instanceof \Traversable) { + $descriptors = $mediaDescriptors; + } else { + $descriptors = Arrays::trimExplode(',', (string)$mediaDescriptors); + } + + $srcsetType = null; + $maxScaleFactor = min($this->baseWidth / $this->targetWidth, $this->baseHeight / $this->targetHeight); + + foreach ($descriptors as $descriptor) { + $hasDescriptor = preg_match('/^(?[0-9]+)w$|^(?[0-9\\.]+)x$/u', $descriptor, $matches); + + if (!$hasDescriptor) { + $this->logger->warning(sprintf('Invalid media descriptor "%s". Missing type "x" or "w"', $descriptor), LogEnvironment::fromMethodName(__METHOD__)); + continue; + } + + if (!$srcsetType) { + $srcsetType = isset($matches['width']) ? 'width' : 'factor'; + } elseif (($srcsetType === 'width' && isset($matches['factor'])) || ($srcsetType === 'factor' && isset($matches['width']))) { + $this->logger->warning(sprintf('Mixed media descriptors are not valid: [%s]', implode(', ', is_array($descriptors) ? $descriptors : iterator_to_array($descriptors))), LogEnvironment::fromMethodName(__METHOD__)); + break; + } + + if ($srcsetType === 'width') { + $width = (int)$matches['width']; + $scaleFactor = $width / $this->width(); + if (!$allowUpScaling && ($width / $this->baseWidth > 1)) { + $srcsetArray[] = $this->src() . ' ' . $this->baseWidth . 'w'; + } else { + $scaled = $this->scale($scaleFactor, $allowUpScaling); + $srcsetArray[] = $scaled->src() . ' ' . $width . 'w'; + } + } elseif ($srcsetType === 'factor') { + $factor = (float)$matches['factor']; + if ( + !$allowUpScaling && ( + ($this->targetHeight && ($maxScaleFactor < $factor)) || + ($this->targetWidth && ($maxScaleFactor < $factor)) + ) + ) { + $scaled = $this->scale($maxScaleFactor, $allowUpScaling); + $srcsetArray[] = $scaled->src() . ' ' . $maxScaleFactor . 'x'; + } else { + $scaled = $this->scale($factor, $allowUpScaling); + $srcsetArray[] = $scaled->src() . ' ' . $factor . 'x'; + } + } + } + + return implode(', ', array_unique($srcsetArray)); + } } diff --git a/Classes/Domain/AssetImageSource.php b/Classes/Domain/AssetImageSource.php index ca3c244..9a0ce68 100644 --- a/Classes/Domain/AssetImageSource.php +++ b/Classes/Domain/AssetImageSource.php @@ -117,7 +117,7 @@ public function src(): string $async = $this->request ? $this->async : false; $allowCropping = true; - $allowUpScaling = false; + $allowUpScaling = $this->allowUpScaling; $thumbnailConfiguration = new ThumbnailConfiguration( $width, $width, @@ -154,7 +154,7 @@ public function dataSrc(): string $async = false; $allowCropping = true; - $allowUpScaling = false; + $allowUpScaling = $this->allowUpScaling; $thumbnailConfiguration = new ThumbnailConfiguration( $width, $width, diff --git a/Classes/Domain/ScalableImageSourceInterface.php b/Classes/Domain/ScalableImageSourceInterface.php index 9883ded..72f8e62 100644 --- a/Classes/Domain/ScalableImageSourceInterface.php +++ b/Classes/Domain/ScalableImageSourceInterface.php @@ -6,5 +6,5 @@ interface ScalableImageSourceInterface extends ImageSourceInterface { - public function scale(float $factor): ImageSourceInterface; + public function scale(float $factor, bool $allowUpScaling = false): ImageSourceInterface; } diff --git a/Resources/Private/Fusion/Prototypes/Image.fusion b/Resources/Private/Fusion/Prototypes/Image.fusion index 76f5a48..3e9e7f1 100644 --- a/Resources/Private/Fusion/Prototypes/Image.fusion +++ b/Resources/Private/Fusion/Prototypes/Image.fusion @@ -126,6 +126,8 @@ prototype(Sitegeist.Kaleidoscope:Image) < prototype(Neos.Fusion:Component) { format = null attributes = Neos.Fusion:DataStructure renderDimensionAttributes = true + allowSrcsetUpScaling = false + preserveAspect = true renderer = Neos.Fusion:Component { @if.hasImageSource = ${props.imageSource && Type.instance(props.imageSource, '\\Sitegeist\\Kaleidoscope\\Domain\\ImageSourceInterface')} @@ -133,8 +135,9 @@ prototype(Sitegeist.Kaleidoscope:Image) < prototype(Neos.Fusion:Component) { # apply format, width and height to the imageSource imageSource = ${props.imageSource} - imageSource.@process.applyWidth = ${props.width ? value.withWidth(props.width) : value} - imageSource.@process.applyHeight = ${props.height ? value.withHeight(props.height) : value} + imageSource.@process.applyDimensions = ${(props.width && props.height) ? value.withDimensions(props.width, props.height) : value} + imageSource.@process.applyWidth = ${(props.width && !props.height) ? value.withWidth(props.width, props.preserveAspect) : value} + imageSource.@process.applyHeight = ${(props.height && !props.width) ? value.withHeight(props.height, props.preserveAspect) : value} imageSource.@process.applyFormat = ${props.format ? value.withFormat(props.format) : value} srcset = ${props.srcset} @@ -145,11 +148,12 @@ prototype(Sitegeist.Kaleidoscope:Image) < prototype(Neos.Fusion:Component) { class = ${props.class} attributes = ${props.attributes} renderDimensionAttributes = ${props.renderDimensionAttributes} + allowSrcsetUpScaling = ${props.allowSrcsetUpScaling} renderer = afx` @@ -114,6 +119,8 @@ prototype(Sitegeist.Kaleidoscope:Picture) < prototype(Neos.Fusion:Component) { srcset={source.srcset ? source.srcset : props.srcset} sizes={source.sizes ? source.sizes : props.sizes} renderDimensionAttributes={props.renderDimensionAttributes} + allowSrcsetUpScaling={props.allowSrcsetUpScaling} + preserveAspect={props.preserveAspect} /> ` diff --git a/Resources/Private/Fusion/Prototypes/Source.fusion b/Resources/Private/Fusion/Prototypes/Source.fusion index 3939ed7..0fba9c5 100644 --- a/Resources/Private/Fusion/Prototypes/Source.fusion +++ b/Resources/Private/Fusion/Prototypes/Source.fusion @@ -13,6 +13,8 @@ prototype(Sitegeist.Kaleidoscope:Source) < prototype(Neos.Fusion:Component) { type = null media = null renderDimensionAttributes = true + allowSrcsetUpScaling = false + preserveAspect = true renderer = Neos.Fusion:Component { @@ -29,19 +31,21 @@ prototype(Sitegeist.Kaleidoscope:Source) < prototype(Neos.Fusion:Component) { isScalableSource = ${imageSource && Type.instance(imageSource, '\\Sitegeist\\Kaleidoscope\\Domain\\ScalableImageSourceInterface')} imageSource = ${imageSource} - imageSource.@process.applyWidth = ${width ? value.withWidth(width) : value} - imageSource.@process.applyHeight = ${height ? value.withHeight(height) : value} - imageSource.@process.applyFormat = ${format ? value.withFormat(format) : value} + imageSource.@process.applyDimensions = ${(props.width && props.height) ? value.withDimensions(props.width, props.height) : value} + imageSource.@process.applyWidth = ${(props.width && !props.height) ? value.withWidth(props.width, props.preserveAspect) : value} + imageSource.@process.applyHeight = ${(props.height && !props.width) ? value.withHeight(props.height, props.preserveAspect) : value} + imageSource.@process.applyFormat = ${props.format ? value.withFormat(props.format) : value} type = ${format ? 'image/' + format : props.type} srcset = ${srcset} sizes = ${sizes} media = ${props.media} renderDimensionAttributes = ${props.renderDimensionAttributes} + allowSrcsetUpScaling = ${props.allowSrcsetUpScaling} renderer = afx`