From 855abfc1375ddd76970a3acf9397131aca11bffe Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Tue, 4 Jun 2024 22:23:24 +0200 Subject: [PATCH 1/4] WIP: Basic infrastructure for focal points in thumbnails and image variants No actual calculation yet --- .../Model/FocalPointSupportInterface.php | 28 ++++++++++ .../Classes/Domain/Model/FocalPointTrait.php | 53 +++++++++++++++++++ .../Classes/Domain/Model/ImageVariant.php | 3 +- Neos.Media/Classes/Domain/Model/Thumbnail.php | 3 +- .../ImageThumbnailGenerator.php | 5 ++ .../Domain/Service/ThumbnailService.php | 12 +++++ .../Mysql/Version20240604184831.php | 36 +++++++++++++ 7 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 Neos.Media/Classes/Domain/Model/FocalPointSupportInterface.php create mode 100644 Neos.Media/Classes/Domain/Model/FocalPointTrait.php create mode 100644 Neos.Media/Migrations/Mysql/Version20240604184831.php diff --git a/Neos.Media/Classes/Domain/Model/FocalPointSupportInterface.php b/Neos.Media/Classes/Domain/Model/FocalPointSupportInterface.php new file mode 100644 index 00000000000..e23cc46b884 --- /dev/null +++ b/Neos.Media/Classes/Domain/Model/FocalPointSupportInterface.php @@ -0,0 +1,28 @@ +focalPointX; + } + + public function setFocalPointX(?int $x): void + { + $this->focalPointX = $x; + } + + public function getFocalPointY(): ?int + { + return $this->focalPointY; + } + + public function setFocalPointY(?int $y): void + { + $this->focalPointY = $y; + } +} diff --git a/Neos.Media/Classes/Domain/Model/ImageVariant.php b/Neos.Media/Classes/Domain/Model/ImageVariant.php index 01a6b6476a3..6546fdd970b 100644 --- a/Neos.Media/Classes/Domain/Model/ImageVariant.php +++ b/Neos.Media/Classes/Domain/Model/ImageVariant.php @@ -32,9 +32,10 @@ * * @Flow\Entity */ -class ImageVariant extends Asset implements AssetVariantInterface, ImageInterface +class ImageVariant extends Asset implements AssetVariantInterface, ImageInterface, FocalPointSupportInterface { use DimensionsTrait; + use FocalPointTrait; /** * @var ImageService diff --git a/Neos.Media/Classes/Domain/Model/Thumbnail.php b/Neos.Media/Classes/Domain/Model/Thumbnail.php index 56d3cf87c82..873ccde897c 100644 --- a/Neos.Media/Classes/Domain/Model/Thumbnail.php +++ b/Neos.Media/Classes/Domain/Model/Thumbnail.php @@ -29,10 +29,11 @@ * } * ) */ -class Thumbnail implements ImageInterface +class Thumbnail implements ImageInterface, FocalPointSupportInterface { use DimensionsTrait; use QualityTrait; + use FocalPointTrait; /** * @var ThumbnailGeneratorStrategy diff --git a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php index 8d8058cda3a..1710a6f5bff 100644 --- a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php +++ b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php @@ -57,6 +57,11 @@ public function canRefresh(Thumbnail $thumbnail) public function refresh(Thumbnail $thumbnail) { try { + /** + * @todo ... add additional crop to ensure that the focal point is + * in view after resizing ... needs common understanding wit + * the thumbnail service here: Packages/Neos/Neos.Media/Classes/Domain/Service/ThumbnailService.php:151 + */ $adjustments = [ new ResizeImageAdjustment( [ diff --git a/Neos.Media/Classes/Domain/Service/ThumbnailService.php b/Neos.Media/Classes/Domain/Service/ThumbnailService.php index 32843c971c3..55e5a141bd9 100644 --- a/Neos.Media/Classes/Domain/Service/ThumbnailService.php +++ b/Neos.Media/Classes/Domain/Service/ThumbnailService.php @@ -19,6 +19,7 @@ use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; +use Neos\Media\Domain\Model\FocalPointSupportInterface; use Neos\Media\Domain\Model\ImageInterface; use Neos\Media\Domain\Model\Thumbnail; use Neos\Media\Domain\Model\ThumbnailConfiguration; @@ -147,6 +148,17 @@ public function getThumbnail(AssetInterface $asset, ThumbnailConfiguration $conf if ($thumbnail === null) { $thumbnail = new Thumbnail($asset, $configuration); + if ($asset instanceof FocalPointSupportInterface) { + // @todo: needs common understanding of dimension change with resize adjustment + // - if a focal point was set + // - calculate target dimensions here + // - calculate new focalPointAfter transformation + // - store focal point in new image + // has to work closely with: Packages/Neos/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php:58 + $thumbnail->setFocalPointX($asset->getFocalPointX() ? $asset->getFocalPointX() + 1 : null); + $thumbnail->setFocalPointY($asset->getFocalPointY() ? $asset->getFocalPointY() + 1 : null); + } + // If the thumbnail strategy failed to generate a valid thumbnail if ($async === false && $thumbnail->getResource() === null && $thumbnail->getStaticResource() === null) { // the thumbnail should not be persisted at this point, but remove is a no-op if the thumbnail diff --git a/Neos.Media/Migrations/Mysql/Version20240604184831.php b/Neos.Media/Migrations/Mysql/Version20240604184831.php new file mode 100644 index 00000000000..f7429b9014f --- /dev/null +++ b/Neos.Media/Migrations/Mysql/Version20240604184831.php @@ -0,0 +1,36 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on "mysql".'); + + $this->addSql('ALTER TABLE neos_media_domain_model_imagevariant ADD focalpointx INT DEFAULT NULL, ADD focalpointy INT DEFAULT NULL'); + $this->addSql('ALTER TABLE neos_media_domain_model_thumbnail ADD focalpointx INT DEFAULT NULL, ADD focalpointy INT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on "mysql".'); + + $this->addSql('ALTER TABLE neos_media_domain_model_imagevariant DROP focalpointx, DROP focalpointy'); + $this->addSql('ALTER TABLE neos_media_domain_model_thumbnail DROP focalpointx, DROP focalpointy'); + + } +} From 78f1b1fb1fd825e78a5bc3ad2853c55f91f755a8 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Wed, 5 Jun 2024 11:46:17 +0200 Subject: [PATCH 2/4] TASK: Extract dimension calculation into ImageDimensionCalculationHelperThingy This obviously will need a better name once we better understand the tasks it will have to perform. --- .../ImageDimensionCalculationHelperThingy.php | 188 ++++++++++++++++++ .../Adjustment/ResizeImageAdjustment.php | 184 ++++------------- .../ImageThumbnailGenerator.php | 2 +- ...geDimensionCalculationHelperThingyTest.php | 177 +++++++++++++++++ 4 files changed, 410 insertions(+), 141 deletions(-) create mode 100644 Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php create mode 100644 Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php diff --git a/Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php b/Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php new file mode 100644 index 00000000000..50f84153fd5 --- /dev/null +++ b/Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php @@ -0,0 +1,188 @@ +getWidth() > $maximumWidth) { + $newDimensions = $newDimensions->widen($maximumWidth); + } + + if ($maximumHeight !== null && $newDimensions->getHeight() > $maximumHeight) { + $newDimensions = $newDimensions->heighten($maximumHeight); + } + + return $newDimensions; + } + + /** + * @param BoxInterface $originalDimensions + * @param integer $requestedWidth + * @param integer $requestedHeight + * @param bool $allowUpScaling + * @param string $ratioMode + * @return BoxInterface + */ + protected static function calculateWithFixedDimensions(BoxInterface $originalDimensions, int $requestedWidth, int $requestedHeight, bool $allowUpScaling = false, string $ratioMode = ImageInterface::RATIOMODE_INSET): BoxInterface + { + if ($ratioMode === ImageInterface::RATIOMODE_OUTBOUND) { + return self::calculateOutboundBox($originalDimensions, $requestedWidth, $requestedHeight, $allowUpScaling); + } + + $newDimensions = clone $originalDimensions; + + $ratios = [ + $requestedWidth / $originalDimensions->getWidth(), + $requestedHeight / $originalDimensions->getHeight() + ]; + + $ratio = min($ratios); + $newDimensions = $newDimensions->scale($ratio); + + if ($allowUpScaling === false && $originalDimensions->contains($newDimensions) === false) { + return clone $originalDimensions; + } + + return $newDimensions; + } + + /** + * Calculate the final dimensions for an outbound box. usually exactly the requested width and height unless that + * would require upscaling and it is not allowed. + * + * @param BoxInterface $originalDimensions + * @param integer $requestedWidth + * @param integer $requestedHeight + * @param bool $allowUpScaling + * @return BoxInterface + */ + protected static function calculateOutboundBox(BoxInterface $originalDimensions, int $requestedWidth, int $requestedHeight, bool $allowUpScaling): BoxInterface + { + $newDimensions = new Box($requestedWidth, $requestedHeight); + + if ($allowUpScaling === true || $originalDimensions->contains($newDimensions) === true) { + return $newDimensions; + } + + // We need to make sure that the new dimensions are such that no upscaling is needed. + $ratios = [ + $originalDimensions->getWidth() / $requestedWidth, + $originalDimensions->getHeight() / $requestedHeight + ]; + + $ratio = min($ratios); + $newDimensions = $newDimensions->scale($ratio); + + return $newDimensions; + } + + /** + * Calculates new dimensions with a requested width applied. Takes upscaling into consideration. + * + * @param BoxInterface $originalDimensions + * @param integer $requestedWidth + * @param bool $allowUpScaling + * @return BoxInterface + */ + protected static function calculateScalingToWidth(BoxInterface $originalDimensions, int $requestedWidth, bool $allowUpScaling): BoxInterface + { + if ($allowUpScaling === false && $requestedWidth >= $originalDimensions->getWidth()) { + return $originalDimensions; + } + + $newDimensions = clone $originalDimensions; + $newDimensions = $newDimensions->widen($requestedWidth); + + return $newDimensions; + } + + /** + * Calculates new dimensions with a requested height applied. Takes upscaling into consideration. + * + * @param BoxInterface $originalDimensions + * @param integer $requestedHeight + * @param bool $allowUpScaling + * @return BoxInterface + */ + protected static function calculateScalingToHeight(BoxInterface $originalDimensions, int $requestedHeight, bool $allowUpScaling): BoxInterface + { + if ($allowUpScaling === false && $requestedHeight >= $originalDimensions->getHeight()) { + return $originalDimensions; + } + + $newDimensions = clone $originalDimensions; + $newDimensions = $newDimensions->heighten($requestedHeight); + + return $newDimensions; + } + + /** + * Calculates a resize dimension box that allows for outbound resize. + * The scaled image will be bigger than the requested dimensions in one dimension and then cropped. + * + * @param BoxInterface $imageSize + * @param BoxInterface $requestedDimensions + * @return BoxInterface + */ + public static function calculateFinalDimensions(BoxInterface $imageSize, BoxInterface $requestedDimensions, string $ratioMode = ImageInterface::RATIOMODE_INSET): BoxInterface + { + if ($ratioMode === ImageInterface::RATIOMODE_OUTBOUND) { + $ratios = [ + $requestedDimensions->getWidth() / $imageSize->getWidth(), + $requestedDimensions->getHeight() / $imageSize->getHeight() + ]; + + return $imageSize->scale(max($ratios)); + } + return $requestedDimensions; + } +} diff --git a/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php index c94d5c43e89..3c07bf5868a 100644 --- a/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php +++ b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php @@ -288,7 +288,15 @@ public function setAllowUpScaling(bool $allowUpScaling): void */ public function canBeApplied(ImagineImageInterface $image) { - $expectedDimensions = $this->calculateDimensions($image->getSize()); + $expectedDimensions = ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + $image->getSize(), + $this->width, + $this->height, + $this->maximumWidth, + $this->maximumHeight, + $this->allowUpScaling ?? false, + $this->ratioMode ?? ImageInterface::RATIOMODE_INSET + ); return ((string)$expectedDimensions !== (string)$image->getSize()); } @@ -311,132 +319,19 @@ public function applyToImage(ImagineImageInterface $image) * * @param BoxInterface $originalDimensions Dimensions of the unadjusted image * @return BoxInterface + * @deprecated use ImageDimensionCalculationHelperThingy::calculateRequestedDimensions instead */ protected function calculateDimensions(BoxInterface $originalDimensions): BoxInterface { - $newDimensions = clone $originalDimensions; - - switch (true) { - // height and width are set explicitly: - case ($this->width !== null && $this->height !== null): - $newDimensions = $this->calculateWithFixedDimensions($originalDimensions, $this->width, $this->height); - break; - // only width is set explicitly: - case ($this->width !== null): - $newDimensions = $this->calculateScalingToWidth($originalDimensions, $this->width); - break; - // only height is set explicitly: - case ($this->height !== null): - $newDimensions = $this->calculateScalingToHeight($originalDimensions, $this->height); - break; - } - - // We apply maximum dimensions and scale the new dimensions proportionally down to fit into the maximum. - if ($this->maximumWidth !== null && $newDimensions->getWidth() > $this->maximumWidth) { - $newDimensions = $newDimensions->widen($this->maximumWidth); - } - - if ($this->maximumHeight !== null && $newDimensions->getHeight() > $this->maximumHeight) { - $newDimensions = $newDimensions->heighten($this->maximumHeight); - } - - return $newDimensions; - } - - /** - * @param BoxInterface $originalDimensions - * @param integer $requestedWidth - * @param integer $requestedHeight - * @return BoxInterface - */ - protected function calculateWithFixedDimensions(BoxInterface $originalDimensions, int $requestedWidth, int $requestedHeight): BoxInterface - { - if ($this->ratioMode === ImageInterface::RATIOMODE_OUTBOUND) { - return $this->calculateOutboundBox($originalDimensions, $requestedWidth, $requestedHeight); - } - - $newDimensions = clone $originalDimensions; - - $ratios = [ - $requestedWidth / $originalDimensions->getWidth(), - $requestedHeight / $originalDimensions->getHeight() - ]; - - $ratio = min($ratios); - $newDimensions = $newDimensions->scale($ratio); - - if ($this->getAllowUpScaling() === false && $originalDimensions->contains($newDimensions) === false) { - return clone $originalDimensions; - } - - return $newDimensions; - } - - /** - * Calculate the final dimensions for an outbound box. usually exactly the requested width and height unless that - * would require upscaling and it is not allowed. - * - * @param BoxInterface $originalDimensions - * @param integer $requestedWidth - * @param integer $requestedHeight - * @return BoxInterface - */ - protected function calculateOutboundBox(BoxInterface $originalDimensions, int $requestedWidth, int $requestedHeight): BoxInterface - { - $newDimensions = new Box($requestedWidth, $requestedHeight); - - if ($this->getAllowUpScaling() === true || $originalDimensions->contains($newDimensions) === true) { - return $newDimensions; - } - - // We need to make sure that the new dimensions are such that no upscaling is needed. - $ratios = [ - $originalDimensions->getWidth() / $requestedWidth, - $originalDimensions->getHeight() / $requestedHeight - ]; - - $ratio = min($ratios); - $newDimensions = $newDimensions->scale($ratio); - - return $newDimensions; - } - - /** - * Calculates new dimensions with a requested width applied. Takes upscaling into consideration. - * - * @param BoxInterface $originalDimensions - * @param integer $requestedWidth - * @return BoxInterface - */ - protected function calculateScalingToWidth(BoxInterface $originalDimensions, int $requestedWidth): BoxInterface - { - if ($this->getAllowUpScaling() === false && $requestedWidth >= $originalDimensions->getWidth()) { - return $originalDimensions; - } - - $newDimensions = clone $originalDimensions; - $newDimensions = $newDimensions->widen($requestedWidth); - - return $newDimensions; - } - - /** - * Calculates new dimensions with a requested height applied. Takes upscaling into consideration. - * - * @param BoxInterface $originalDimensions - * @param integer $requestedHeight - * @return BoxInterface - */ - protected function calculateScalingToHeight(BoxInterface $originalDimensions, int $requestedHeight): BoxInterface - { - if ($this->getAllowUpScaling() === false && $requestedHeight >= $originalDimensions->getHeight()) { - return $originalDimensions; - } - - $newDimensions = clone $originalDimensions; - $newDimensions = $newDimensions->heighten($requestedHeight); - - return $newDimensions; + return ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + $originalDimensions, + $this->width, + $this->height, + $this->maximumWidth, + $this->maximumHeight, + $this->allowUpScaling ?? false, + $this->ratioMode ?? ImageInterface::RATIOMODE_INSET + ); } /** @@ -456,22 +351,31 @@ protected function resize(ImagineImageInterface $image, string $mode = ImageInte throw new \InvalidArgumentException('Invalid mode specified', 1574686891); } - $imageSize = $image->getSize(); - $requestedDimensions = $this->calculateDimensions($imageSize); + $originalDimensions = $image->getSize(); - $image->strip(); + $requestedDimensions = ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + $originalDimensions, + $this->width, + $this->height, + $this->maximumWidth, + $this->maximumHeight, + $this->allowUpScaling ?? false, + $this->ratioMode ?? ImageInterface::RATIOMODE_INSET + ); - $resizeDimensions = $requestedDimensions; - if ($mode === ImageInterface::RATIOMODE_OUTBOUND) { - $resizeDimensions = $this->calculateOutboundScalingDimensions($imageSize, $requestedDimensions); - } + $finalDimensions = ImageDimensionCalculationHelperThingy::calculateFinalDimensions( + $originalDimensions, + $requestedDimensions, + $this->ratioMode + ); - $image->resize($resizeDimensions, $filter); + $image->strip(); + $image->resize($finalDimensions, $filter); if ($mode === ImageInterface::RATIOMODE_OUTBOUND) { $image->crop(new Point( - max(0, round(($resizeDimensions->getWidth() - $requestedDimensions->getWidth()) / 2)), - max(0, round(($resizeDimensions->getHeight() - $requestedDimensions->getHeight()) / 2)) + max(0, round(($finalDimensions->getWidth() - $requestedDimensions->getWidth()) / 2)), + max(0, round(($finalDimensions->getHeight() - $requestedDimensions->getHeight()) / 2)) ), $requestedDimensions); } @@ -485,14 +389,14 @@ protected function resize(ImagineImageInterface $image, string $mode = ImageInte * @param BoxInterface $imageSize * @param BoxInterface $requestedDimensions * @return BoxInterface + * @deprecated use ImageDimensionCalculationHelperThingy::calculateFinalDimensions instead */ protected function calculateOutboundScalingDimensions(BoxInterface $imageSize, BoxInterface $requestedDimensions): BoxInterface { - $ratios = [ - $requestedDimensions->getWidth() / $imageSize->getWidth(), - $requestedDimensions->getHeight() / $imageSize->getHeight() - ]; - - return $imageSize->scale(max($ratios)); + return ImageDimensionCalculationHelperThingy::calculateFinalDimensions( + $imageSize, + $requestedDimensions, + $this->ratioMode + ); } } diff --git a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php index 1710a6f5bff..692646a9be6 100644 --- a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php +++ b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php @@ -58,7 +58,7 @@ public function refresh(Thumbnail $thumbnail) { try { /** - * @todo ... add additional crop to ensure that the focal point is + * @todo ... add additional crop to ensure that the focal point is in view * in view after resizing ... needs common understanding wit * the thumbnail service here: Packages/Neos/Neos.Media/Classes/Domain/Service/ThumbnailService.php:151 */ diff --git a/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php b/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php new file mode 100644 index 00000000000..afbfcbe0d4a --- /dev/null +++ b/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php @@ -0,0 +1,177 @@ + Date: Fri, 28 Jun 2024 12:59:17 +0200 Subject: [PATCH 3/4] TASK: Persist focal points with thumbnail --- Neos.Media/Classes/Domain/Model/ThumbnailConfiguration.php | 2 +- Neos.Media/Classes/Domain/Repository/ThumbnailRepository.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Neos.Media/Classes/Domain/Model/ThumbnailConfiguration.php b/Neos.Media/Classes/Domain/Model/ThumbnailConfiguration.php index a24b015fba8..936df98e08e 100644 --- a/Neos.Media/Classes/Domain/Model/ThumbnailConfiguration.php +++ b/Neos.Media/Classes/Domain/Model/ThumbnailConfiguration.php @@ -123,7 +123,7 @@ public function getMaximumHeight() } /** - * @return boolean + * @return string */ public function getRatioMode() { diff --git a/Neos.Media/Classes/Domain/Repository/ThumbnailRepository.php b/Neos.Media/Classes/Domain/Repository/ThumbnailRepository.php index 0fd34f67e0d..326b5292c67 100644 --- a/Neos.Media/Classes/Domain/Repository/ThumbnailRepository.php +++ b/Neos.Media/Classes/Domain/Repository/ThumbnailRepository.php @@ -152,7 +152,7 @@ public function persistThumbnailDirectly(Thumbnail $thumbnail, ThumbnailConfigur $assetIdentifier = $this->persistenceManager->getIdentifierByObject($thumbnail->getOriginalAsset()); $thumbnailResource = $thumbnail->getResource(); - $sql = 'INSERT INTO neos_media_domain_model_thumbnail (persistence_object_identifier, originalasset, resource, width, height, configuration, configurationhash, staticresource, quality) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $sql = 'INSERT INTO neos_media_domain_model_thumbnail (persistence_object_identifier, originalasset, resource, width, height, configuration, configurationhash, staticresource, quality, focalpointx, focalpointy) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $params = [ $thumbnailIdentifier, $assetIdentifier, @@ -163,6 +163,8 @@ public function persistThumbnailDirectly(Thumbnail $thumbnail, ThumbnailConfigur $configuration->getHash(), $thumbnail->getStaticResource(), $thumbnail->getQuality(), + $thumbnail->getFocalPointX(), + $thumbnail->getFocalPointY(), ]; $connection = $this->entityManager->getConnection(); From 4265151369797c2d41d3418a73ce4335696a198f Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 12 Jul 2024 18:53:00 +0200 Subject: [PATCH 4/4] TASK: Calculate a preliminary crop to the target aspect for images with focalPoint The preliminary crop ensures: 1. The target image is as large as possible inside the original image dimensions 2. The focal point is as central as possible inside the generated image --- .../Model/Adjustment/MarkPointAdjustment.php | 94 ++++++++++++ ...ingy.php => ResizeDimensionCalculator.php} | 68 ++++++++- .../Adjustment/ResizeImageAdjustment.php | 14 +- .../Dto/PreliminaryCropSpecification.php | 35 +++++ .../Model/FocalPointSupportInterface.php | 6 + .../Classes/Domain/Model/FocalPointTrait.php | 18 +++ .../ImageThumbnailGenerator.php | 69 ++++++++- .../Domain/Service/ThumbnailService.php | 48 +++++-- ....php => ResizeDimensionCalculatorTest.php} | 134 ++++++++++++++++-- 9 files changed, 453 insertions(+), 33 deletions(-) create mode 100644 Neos.Media/Classes/Domain/Model/Adjustment/MarkPointAdjustment.php rename Neos.Media/Classes/Domain/Model/Adjustment/{ImageDimensionCalculationHelperThingy.php => ResizeDimensionCalculator.php} (65%) create mode 100644 Neos.Media/Classes/Domain/Model/Dto/PreliminaryCropSpecification.php rename Neos.Media/Tests/Unit/Domain/Model/Adjustment/{ImageDimensionCalculationHelperThingyTest.php => ResizeDimensionCalculatorTest.php} (56%) diff --git a/Neos.Media/Classes/Domain/Model/Adjustment/MarkPointAdjustment.php b/Neos.Media/Classes/Domain/Model/Adjustment/MarkPointAdjustment.php new file mode 100644 index 00000000000..989bb67aee9 --- /dev/null +++ b/Neos.Media/Classes/Domain/Model/Adjustment/MarkPointAdjustment.php @@ -0,0 +1,94 @@ +x = $x; + } + + public function setY(int $y): void + { + $this->y = $y; + } + + public function setRadius(int $radius): void + { + $this->radius = $radius; + } + + public function setThickness(int $thickness): void + { + $this->thickness = $thickness; + } + + public function setColor(string $color): void + { + $this->color = $color; + } + + + public function applyToImage(ImagineImageInterface $image) + { + $palette = new Palette\RGB(); + $color = $palette->color($this->color); + $image->draw() + ->circle( + new Point($this->x, $this->y), + $this->radius, + $color, + false, + $this->thickness + ) + ; + + return $image; + } + + public function canBeApplied(ImagineImageInterface $image) + { + if (is_null($this->x) || is_null($this->y) || is_null($this->radius)) { + return false; + } + return true; + } +} diff --git a/Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeDimensionCalculator.php similarity index 65% rename from Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php rename to Neos.Media/Classes/Domain/Model/Adjustment/ResizeDimensionCalculator.php index 50f84153fd5..033ea9b222d 100644 --- a/Neos.Media/Classes/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingy.php +++ b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeDimensionCalculator.php @@ -14,10 +14,22 @@ */ use Imagine\Image\BoxInterface; +use Imagine\Image\Point; +use Imagine\Image\PointInterface; +use Neos\Media\Domain\Model\Dto\PreliminaryCropSpecification; use Neos\Media\Domain\Model\ImageInterface; +use Neos\Media\Domain\ValueObject\Configuration\AspectRatio; use Neos\Media\Imagine\Box; -class ImageDimensionCalculationHelperThingy +/** + * Container for static methods to calculate the target dimensions for resizing images + * + * @see: ResizeImageAdjustment, ImageThumbnailGenerator(to calculte a preliminary crop for images with focal point), + * ThumbnailService(to calculate dimensions and focal points for async thumbnails) + * + * @internal + */ +class ResizeDimensionCalculator { /** * @param BoxInterface $originalDimensions @@ -173,7 +185,7 @@ protected static function calculateScalingToHeight(BoxInterface $originalDimensi * @param BoxInterface $requestedDimensions * @return BoxInterface */ - public static function calculateFinalDimensions(BoxInterface $imageSize, BoxInterface $requestedDimensions, string $ratioMode = ImageInterface::RATIOMODE_INSET): BoxInterface + public static function calculateOutboundScalingDimensions(BoxInterface $imageSize, BoxInterface $requestedDimensions, string $ratioMode = ImageInterface::RATIOMODE_INSET): BoxInterface { if ($ratioMode === ImageInterface::RATIOMODE_OUTBOUND) { $ratios = [ @@ -185,4 +197,56 @@ public static function calculateFinalDimensions(BoxInterface $imageSize, BoxInte } return $requestedDimensions; } + + /** + * Calculate the informations for a preliminary crop to ensure that the given focal point stays inside the final image + * with the requested dimensions + * + * - The cropDimensions have the aspect of requested dimensions and have the maximal possible dimensions + * - The cropOffset will position the crop with the focal point as close to the center as possible + * - The returned focal point is the position of the focal point after the crop inside the requested dimensions + */ + public static function calculatePreliminaryCropSpecification( + BoxInterface $originalDimensions, + PointInterface $originalFocalPoint, + BoxInterface $targetDimensions, + ): PreliminaryCropSpecification { + $originalAspect = new AspectRatio($originalDimensions->getWidth(), $originalDimensions->getHeight()); + $targetAspect = new AspectRatio($targetDimensions->getWidth(), $targetDimensions->getHeight()); + + if ($originalAspect->getRatio() >= $targetAspect->getRatio()) { + // target-aspect is wider as original-aspect or same: use full height, width is cropped + $factor = $originalDimensions->getHeight() / $targetDimensions->getHeight(); + $cropDimensions = new \Imagine\Image\Box((int)($targetDimensions->getWidth() * $factor), $originalDimensions->getHeight()); + $cropOffsetX = $originalFocalPoint->getX() - (int)($cropDimensions->getWidth() / 2); + $cropOffsetXMax = $originalDimensions->getWidth() - $cropDimensions->getWidth(); + if ($cropOffsetX < 0) { + $cropOffsetX = 0; + } elseif ($cropOffsetX > $cropOffsetXMax) { + $cropOffsetX = $cropOffsetXMax; + } + $cropOffset = new Point($cropOffsetX, 0); + } else { + // target-aspect is higher than original-aspect: use full width, height is cropped + $factor = $originalDimensions->getWidth() / $targetDimensions->getWidth(); + $cropDimensions = new Box($originalDimensions->getWidth(), (int)($targetDimensions->getHeight() * $factor)); + $cropOffsetY = $originalFocalPoint->getY() - (int)($cropDimensions->getHeight() / 2); + $cropOffsetYMax = $originalDimensions->getHeight() - $cropDimensions->getHeight(); + if ($cropOffsetY < 0) { + $cropOffsetY = 0; + } elseif ($cropOffsetY > $cropOffsetYMax) { + $cropOffsetY = $cropOffsetYMax; + } + $cropOffset = new Point(0, $cropOffsetY); + } + + return new PreliminaryCropSpecification( + $cropOffset, + $cropDimensions, + new Point( + (int)round(($originalFocalPoint->getX() - $cropOffset->getX()) / $factor), + (int)round(($originalFocalPoint->getY() - $cropOffset->getY()) / $factor) + ) + ); + } } diff --git a/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php index 3c07bf5868a..425990eccbc 100644 --- a/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php +++ b/Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php @@ -288,7 +288,7 @@ public function setAllowUpScaling(bool $allowUpScaling): void */ public function canBeApplied(ImagineImageInterface $image) { - $expectedDimensions = ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + $expectedDimensions = ResizeDimensionCalculator::calculateRequestedDimensions( $image->getSize(), $this->width, $this->height, @@ -319,11 +319,11 @@ public function applyToImage(ImagineImageInterface $image) * * @param BoxInterface $originalDimensions Dimensions of the unadjusted image * @return BoxInterface - * @deprecated use ImageDimensionCalculationHelperThingy::calculateRequestedDimensions instead + * @deprecated use ResizeDimensionCalculator::calculateRequestedDimensions instead */ protected function calculateDimensions(BoxInterface $originalDimensions): BoxInterface { - return ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + return ResizeDimensionCalculator::calculateRequestedDimensions( $originalDimensions, $this->width, $this->height, @@ -353,7 +353,7 @@ protected function resize(ImagineImageInterface $image, string $mode = ImageInte $originalDimensions = $image->getSize(); - $requestedDimensions = ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + $requestedDimensions = ResizeDimensionCalculator::calculateRequestedDimensions( $originalDimensions, $this->width, $this->height, @@ -363,7 +363,7 @@ protected function resize(ImagineImageInterface $image, string $mode = ImageInte $this->ratioMode ?? ImageInterface::RATIOMODE_INSET ); - $finalDimensions = ImageDimensionCalculationHelperThingy::calculateFinalDimensions( + $finalDimensions = ResizeDimensionCalculator::calculateOutboundScalingDimensions( $originalDimensions, $requestedDimensions, $this->ratioMode @@ -389,11 +389,11 @@ protected function resize(ImagineImageInterface $image, string $mode = ImageInte * @param BoxInterface $imageSize * @param BoxInterface $requestedDimensions * @return BoxInterface - * @deprecated use ImageDimensionCalculationHelperThingy::calculateFinalDimensions instead + * @deprecated use ResizeDimensionCalculator::calculateOutboundScalingDimensions instead */ protected function calculateOutboundScalingDimensions(BoxInterface $imageSize, BoxInterface $requestedDimensions): BoxInterface { - return ImageDimensionCalculationHelperThingy::calculateFinalDimensions( + return ResizeDimensionCalculator::calculateOutboundScalingDimensions( $imageSize, $requestedDimensions, $this->ratioMode diff --git a/Neos.Media/Classes/Domain/Model/Dto/PreliminaryCropSpecification.php b/Neos.Media/Classes/Domain/Model/Dto/PreliminaryCropSpecification.php new file mode 100644 index 00000000000..dd29da26a6e --- /dev/null +++ b/Neos.Media/Classes/Domain/Model/Dto/PreliminaryCropSpecification.php @@ -0,0 +1,35 @@ +focalPointY = $y; } + + public function hasFocalPoint(): bool + { + if ($this->focalPointX !== null && $this->focalPointY !== null) { + return true; + } + return false; + } + + public function getFocalPoint(): ?PointInterface + { + if ($this->hasFocalPoint()) { + return new Point($this->focalPointX, $this->focalPointY); + } + return null; + } } diff --git a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php index 692646a9be6..8be8f80a7f0 100644 --- a/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php +++ b/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php @@ -12,12 +12,18 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Media\Domain\Model\Adjustment\CropImageAdjustment; +use Neos\Media\Domain\Model\Adjustment\ResizeDimensionCalculator; +use Neos\Media\Domain\Model\Adjustment\MarkPointAdjustment; use Neos\Media\Domain\Model\Adjustment\QualityImageAdjustment; use Neos\Media\Domain\Model\Adjustment\ResizeImageAdjustment; +use Neos\Media\Domain\Model\Dto\PreliminaryCropSpecification; +use Neos\Media\Domain\Model\FocalPointSupportInterface; use Neos\Media\Domain\Model\ImageInterface; use Neos\Media\Domain\Model\Thumbnail; use Neos\Media\Domain\Service\ImageService; use Neos\Media\Exception; +use Neos\Media\Imagine\Box; /** * A system-generated preview version of an Image @@ -57,11 +63,6 @@ public function canRefresh(Thumbnail $thumbnail) public function refresh(Thumbnail $thumbnail) { try { - /** - * @todo ... add additional crop to ensure that the focal point is in view - * in view after resizing ... needs common understanding wit - * the thumbnail service here: Packages/Neos/Neos.Media/Classes/Domain/Service/ThumbnailService.php:151 - */ $adjustments = [ new ResizeImageAdjustment( [ @@ -80,6 +81,59 @@ public function refresh(Thumbnail $thumbnail) ) ]; + $asset = $thumbnail->getOriginalAsset(); + $preliminaryCropSpecification = null; + if ($asset instanceof FocalPointSupportInterface && $asset->hasFocalPoint()) { + // in case we have a focal point we calculate the target dimension and add an + // preliminary crop to ensure that the focal point stays inside the final image + // while beeing as central as possible + + $originalFocalPoint = $asset->getFocalPoint(); + $originalDimensions = new Box($asset->getWidth(), $asset->getHeight()); + $requestedDimensions = ResizeDimensionCalculator::calculateRequestedDimensions( + originalDimensions: $originalDimensions, + width: $thumbnail->getConfigurationValue('width'), + maximumWidth: $thumbnail->getConfigurationValue('maximumWidth'), + height: $thumbnail->getConfigurationValue('height'), + maximumHeight: $thumbnail->getConfigurationValue('maximumHeight'), + ratioMode: $thumbnail->getConfigurationValue('ratioMode'), + allowUpScaling: $thumbnail->getConfigurationValue('allowUpScaling'), + ); + + $preliminaryCropSpecification = ResizeDimensionCalculator::calculatePreliminaryCropSpecification( + originalDimensions: $originalDimensions, + originalFocalPoint: $originalFocalPoint, + targetDimensions: $requestedDimensions, + ); + + $adjustments = array_merge( + [ + new CropImageAdjustment( + [ + 'x' => $preliminaryCropSpecification->cropOffset->getX(), + 'y' => $preliminaryCropSpecification->cropOffset->getY(), + 'width' => $preliminaryCropSpecification->cropDimensions->getWidth(), + 'height' => $preliminaryCropSpecification->cropDimensions->getHeight(), + ] + ) + ], + $adjustments, + [ + // this is for debugging purposes only + // @todo remove before merging + new MarkPointAdjustment( + [ + 'x' => $preliminaryCropSpecification->focalPoint->getX(), + 'y' => $preliminaryCropSpecification->focalPoint->getY(), + 'radius' => 5, + 'color' => '#0f0', + 'thickness' => 4 + ] + ), + ] + ); + } + $targetFormat = $thumbnail->getConfigurationValue('format'); $processedImageInfo = $this->imageService->processImage($thumbnail->getOriginalAsset()->getResource(), $adjustments, $targetFormat); @@ -87,6 +141,11 @@ public function refresh(Thumbnail $thumbnail) $thumbnail->setWidth($processedImageInfo['width']); $thumbnail->setHeight($processedImageInfo['height']); $thumbnail->setQuality($processedImageInfo['quality']); + + if ($preliminaryCropSpecification instanceof PreliminaryCropSpecification) { + $thumbnail->setFocalPointX($preliminaryCropSpecification->focalPoint->getX()); + $thumbnail->setFocalPointY($preliminaryCropSpecification->focalPoint->getY()); + } } catch (\Exception $exception) { $message = sprintf('Unable to generate thumbnail for the given image (filename: %s, SHA1: %s)', $thumbnail->getOriginalAsset()->getResource()->getFilename(), $thumbnail->getOriginalAsset()->getResource()->getSha1()); throw new Exception\NoThumbnailAvailableException($message, 1433109654, $exception); diff --git a/Neos.Media/Classes/Domain/Service/ThumbnailService.php b/Neos.Media/Classes/Domain/Service/ThumbnailService.php index 55e5a141bd9..b0ac9753ba2 100644 --- a/Neos.Media/Classes/Domain/Service/ThumbnailService.php +++ b/Neos.Media/Classes/Domain/Service/ThumbnailService.php @@ -18,6 +18,7 @@ use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\ResourceManagement\ResourceManager; +use Neos\Media\Domain\Model\Adjustment\ResizeDimensionCalculator; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\FocalPointSupportInterface; use Neos\Media\Domain\Model\ImageInterface; @@ -25,6 +26,7 @@ use Neos\Media\Domain\Model\ThumbnailConfiguration; use Neos\Media\Domain\Repository\ThumbnailRepository; use Neos\Media\Exception\ThumbnailServiceException; +use Neos\Media\Imagine\Box; use Neos\Utility\Arrays; use Neos\Utility\MediaTypes; use Psr\Log\LoggerInterface; @@ -85,6 +87,12 @@ class ThumbnailService */ protected $throwableStorage; + /** + * @var ResizeDimensionCalculator + * @Flow\Inject + */ + protected $imageDimensionCalculationHelperThingy; + /** * Returns a thumbnail of the given asset * @@ -148,15 +156,37 @@ public function getThumbnail(AssetInterface $asset, ThumbnailConfiguration $conf if ($thumbnail === null) { $thumbnail = new Thumbnail($asset, $configuration); - if ($asset instanceof FocalPointSupportInterface) { - // @todo: needs common understanding of dimension change with resize adjustment - // - if a focal point was set - // - calculate target dimensions here - // - calculate new focalPointAfter transformation - // - store focal point in new image - // has to work closely with: Packages/Neos/Neos.Media/Classes/Domain/Model/ThumbnailGenerator/ImageThumbnailGenerator.php:58 - $thumbnail->setFocalPointX($asset->getFocalPointX() ? $asset->getFocalPointX() + 1 : null); - $thumbnail->setFocalPointY($asset->getFocalPointY() ? $asset->getFocalPointY() + 1 : null); + // predict dimensions async image thumbnails, this is not needed for immediately calculated images as those + // values are stored again after calculating + if ($async === true && $asset instanceof ImageInterface) { + $originalDimensions = new Box($asset->getWidth(), $asset->getHeight()); + + $requestedDimensions = ResizeDimensionCalculator::calculateRequestedDimensions( + originalDimensions: $originalDimensions, + width: $configuration->getWidth(), + maximumWidth: $configuration->getMaximumWidth(), + height: $configuration->getHeight(), + maximumHeight: $configuration->getMaximumHeight(), + ratioMode: $configuration->getRatioMode(), + allowUpScaling: $configuration->isUpScalingAllowed() + ); + + $thumbnail->setWidth($requestedDimensions->getWidth()); + $thumbnail->setHeight($requestedDimensions->getHeight()); + + // calculate focal point for new thumbnails + if ($asset instanceof FocalPointSupportInterface && $asset->hasFocalPoint()) { + $originalFocalPoint = $asset->getFocalPoint(); + + $preliminaryCropSpecification = ResizeDimensionCalculator::calculatePreliminaryCropSpecification( + originalDimensions: $originalDimensions, + originalFocalPoint: $originalFocalPoint, + targetDimensions: $requestedDimensions, + ); + + $thumbnail->setFocalPointX($preliminaryCropSpecification->focalPoint->getX()); + $thumbnail->setFocalPointY($preliminaryCropSpecification->focalPoint->getY()); + } } // If the thumbnail strategy failed to generate a valid thumbnail diff --git a/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php b/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ResizeDimensionCalculatorTest.php similarity index 56% rename from Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php rename to Neos.Media/Tests/Unit/Domain/Model/Adjustment/ResizeDimensionCalculatorTest.php index afbfcbe0d4a..495c356eac5 100644 --- a/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ImageDimensionCalculationHelperThingyTest.php +++ b/Neos.Media/Tests/Unit/Domain/Model/Adjustment/ResizeDimensionCalculatorTest.php @@ -11,15 +11,18 @@ * source code. */ -use Neos\Media\Domain\Model\Adjustment\ImageDimensionCalculationHelperThingy; +use Imagine\Image\BoxInterface; +use Imagine\Image\Point; +use Imagine\Image\PointInterface; +use Neos\Media\Domain\Model\Adjustment\ResizeDimensionCalculator; use Neos\Media\Imagine\Box; use Neos\Flow\Tests\UnitTestCase; use Neos\Media\Domain\Model\ImageInterface; /** - * Test case for the ImageDimensionCalculationHelperThingy + * Test case for the ResizeDimensionCalculator */ -class ImageDimensionCalculationHelperThingyTest extends UnitTestCase +class ResizeDimensionCalculatorTest extends UnitTestCase { /** * @test @@ -31,7 +34,7 @@ public function widthAndHeightDeterminedByExplicitlySetWidthAndHeightWithInsetMo self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, width: 110, height: 110, @@ -49,7 +52,7 @@ public function widthAndHeightDeterminedByExplicitlySetWidthAndHeightWithOutboun self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, width: 110, height: 110, @@ -68,7 +71,7 @@ public function ifWidthIsSetHeightIsDeterminedByTheOriginalAspectRatio() self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, width: 110 ) @@ -85,7 +88,7 @@ public function ifHeightIsSetWidthIsDeterminedByTheOriginalAspectRatio() self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, height: 95 ) @@ -102,7 +105,7 @@ public function minimumHeightIsGreaterZero() self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, maximumWidth: 250, maximumHeight: 250, @@ -121,7 +124,7 @@ public function minimumWidthIsGreaterZero() self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, maximumWidth: 250, maximumHeight: 250, @@ -163,7 +166,7 @@ public function combinationsOfMaximumAndMinimumWidthAndHeightAreCalculatedCorrec self::assertEquals( $expectedDimensions, - ImageDimensionCalculationHelperThingy::calculateRequestedDimensions( + ResizeDimensionCalculator::calculateRequestedDimensions( originalDimensions: $originalDimensions, width: $width, height: $height, @@ -174,4 +177,115 @@ public function combinationsOfMaximumAndMinimumWidthAndHeightAreCalculatedCorrec ) ); } + + public static function calculateCropConfigurationCentersFocalPointDataProvider(): \Generator + { + yield 'square to square' => [ + new \Imagine\Image\Box(400, 400), + new Point(200, 200), + new Box(200, 200), + + new Point(0, 0), + new Box(400, 400), + new Point(100, 100), + ]; + + yield 'portrait to portrait' => [ + new Box(800, 400), + new Point(400, 200), + new Box(400, 200), + + new Point(0, 0), + new Box(800, 400), + new Point(200, 100), + ]; + + yield 'portrait to square fp left' => [ + new Box(800, 400), + new Point(50, 200), + new Box(400, 400), + + new Point(0, 0), + new Box(400, 400), + new Point(50, 200), + ]; + + yield 'portrait to square fp center' => [ + new Box(800, 400), + new Point(400, 200), + new Box(400, 400), + + new Point(200, 0), + new Box(400, 400), + new Point(200, 200), + ]; + + yield 'portrait to square fp right' => [ + new Box(800, 400), + new Point(700, 100), + new Box(400, 400), + + new Point(400, 0), + new Box(400, 400), + new Point(300, 100), + ]; + + yield 'landscape to square fp center' => [ + new Box(400, 800), + new Point(200, 400), + new Box(400, 400), + + new Point(0, 200), + new Box(400, 400), + new Point(200, 200), + ]; + + yield 'landscape to square fp top' => [ + new Box(400, 800), + new Point(350, 50), + new Box(400, 400), + + new Point(0, 0), + new Box(400, 400), + new Point(350, 50), + ]; + + yield 'landscape to square fp bottom' => [ + new Box(400, 800), + new Point(300, 750), + new Box(200, 200), + + new Point(0, 400), + new Box(400, 400), + new Point(150, 175), + ]; + } + + /** + * @dataProvider calculateCropConfigurationCentersFocalPointDataProvider + * @test + */ + public function calculateCropConfigurationCentersFocalPoint( + BoxInterface $originalDimensions, + PointInterface $originalFocalPoint, + BoxInterface $requestedDimensions, + PointInterface $expectedCropOffset, + BoxInterface $expectedCropDimensions, + PointInterface $expectedCroppedFocalPoint + ): void { + $preliminaryCropSpecification = ResizeDimensionCalculator::calculatePreliminaryCropSpecification( + $originalDimensions, + $originalFocalPoint, + $requestedDimensions + ); + + $this->assertEquals($expectedCropDimensions->getWidth(), $preliminaryCropSpecification->cropDimensions->getWidth()); + $this->assertEquals($expectedCropDimensions->getHeight(), $preliminaryCropSpecification->cropDimensions->getHeight()); + + $this->assertEquals($expectedCropOffset->getX(), $preliminaryCropSpecification->cropOffset->getX()); + $this->assertEquals($expectedCropOffset->getY(), $preliminaryCropSpecification->cropOffset->getY()); + + $this->assertEquals($expectedCroppedFocalPoint->getX(), $preliminaryCropSpecification->focalPoint->getX()); + $this->assertEquals($expectedCroppedFocalPoint->getY(), $preliminaryCropSpecification->focalPoint->getY()); + } }