Skip to content

Commit

Permalink
TASK: Calculate a preliminary crop to the target aspect for images wi…
Browse files Browse the repository at this point in the history
…th focalPoint

The calculation of the preliminary crop ensures:
1. The crop to the target dimension is as large as possible inside the original image dimensions
2. The crop is placed in a way to ensure the focal point is as central as possible inside the original image dimensions
  • Loading branch information
mficzel committed Jul 25, 2024
1 parent b4eaea5 commit 6bd01c1
Show file tree
Hide file tree
Showing 8 changed files with 412 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
*/

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
Expand Down Expand Up @@ -185,4 +189,48 @@ public static function calculateFinalDimensions(BoxInterface $imageSize, BoxInte
}
return $requestedDimensions;
}

public static function calculatePreliminaryCropSpecification(
BoxInterface $originalDimensions,
PointInterface $originalFocalPoint,
BoxInterface $requestedDimensions,
): PreliminaryCropSpecification {
$originalAspect = new AspectRatio($originalDimensions->getWidth(), $originalDimensions->getHeight());
$finalAspect = new AspectRatio($requestedDimensions->getWidth(), $requestedDimensions->getHeight());

if ($originalAspect->getRatio() >= $finalAspect->getRatio()) {
// leading dimension = height, width is cropped
$factor = $requestedDimensions->getHeight() / $originalDimensions->getHeight();
$cropBox = new \Imagine\Image\Box((int)$requestedDimensions->getWidth() / $factor, $requestedDimensions->getHeight() / $factor);
$cropX = $originalFocalPoint->getX() - (int)($cropBox->getWidth() / 2);
$cropXMax = $originalDimensions->getWidth() - $cropBox->getWidth();
if ($cropX < 0) {
$cropX = 0;
} elseif ($cropX > $cropXMax) {
$cropX = $cropXMax;
}
$cropOffset = new Point($cropX, 0);
} else {
// leading dimension = width, height is cropped
$factor = $requestedDimensions->getWidth() / $originalDimensions->getWidth();
$cropBox = new Box((int)$requestedDimensions->getWidth() / $factor, $requestedDimensions->getHeight() / $factor);
$cropY = $originalFocalPoint->getY() - (int)($cropBox->getHeight() / 2);
$cropYMax = $originalDimensions->getHeight() - $cropBox->getHeight();
if ($cropY < 0) {
$cropY = 0;
} elseif ($cropY > $cropYMax) {
$cropY = $cropYMax;
}
$cropOffset = new Point(0, $cropY);
}

return new PreliminaryCropSpecification(
$cropOffset,
$cropBox,
new Point(
(int)round(($originalFocalPoint->getX() - $cropOffset->getX()) * $factor),
(int)round(($originalFocalPoint->getY() - $cropOffset->getY()) * $factor)
)
);
}
}
94 changes: 94 additions & 0 deletions Neos.Media/Classes/Domain/Model/Adjustment/MarkPointAdjustment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);

namespace Neos\Media\Domain\Model\Adjustment;

/*
* This file is part of the Neos.Media package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Imagine\Image\ImageInterface as ImagineImageInterface;
use Imagine\Image\Point;
use Neos\Flow\Annotations as Flow;
use Imagine\Image\Palette;

/**
* An adjustment to draw on circle on an image
* this is solely for debugging of focal points
* @todo remove before merging
*
* @deprecated
* @Flow\Entity
*/
class MarkPointAdjustment extends AbstractImageAdjustment
{
protected $position = 99;

protected int $x;

protected int $y;

protected int $radius;

protected int $thickness = 1;

protected string $color = '#000';


public function setX(int $x): void
{
$this->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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);

namespace Neos\Media\Domain\Model\Dto;

/*
* This file is part of the Neos.Media package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Imagine\Image\BoxInterface;
use Imagine\Image\PointInterface;

final readonly class PreliminaryCropSpecification
{
public function __construct(
public PointInterface $cropOffset,
public BoxInterface $cropDimensions,
public PointInterface $focalPoint,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* source code.
*/

use Imagine\Image\PointInterface;

/**
* Interface for assets which provide methods for focal points
*/
Expand All @@ -25,4 +27,8 @@ public function setFocalPointX(?int $x): void;
public function getFocalPointY(): ?int;

public function setFocalPointY(?int $y): void;

public function hasFocalPoint(): bool;

public function getFocalPoint(): ?PointInterface;
}
18 changes: 18 additions & 0 deletions Neos.Media/Classes/Domain/Model/FocalPointTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/

use Doctrine\ORM\Mapping as ORM;
use Imagine\Image\Point;
use Imagine\Image\PointInterface;

/**
* Trait for assets which provide methods for focal points
Expand Down Expand Up @@ -50,4 +52,20 @@ public function setFocalPointY(?int $y): void
{
$this->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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@
*/

use Neos\Flow\Annotations as Flow;
use Neos\Media\Domain\Model\Adjustment\CropImageAdjustment;
use Neos\Media\Domain\Model\Adjustment\ImageDimensionCalculationHelperThingy;
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
Expand Down Expand Up @@ -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(
[
Expand All @@ -80,13 +81,73 @@ public function refresh(Thumbnail $thumbnail)
)
];

$asset = $thumbnail->getOriginalAsset();
/**
* @var $preliminaryCropSpecification PreliminaryCropSpecification|null
*/
$preliminaryCropSpecification = null;
if ($asset instanceof FocalPointSupportInterface && $asset->hasFocalPoint()) {
// in case we have a focal point we calculate the target dimension and add an
// additional crop to ensure that the focal point stays inside the final image

$originalFocalPoint = $asset->getFocalPoint();
$originalDimensions = new Box($asset->getWidth(), $asset->getHeight());
$requestedDimensions = ImageDimensionCalculationHelperThingy::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 = ImageDimensionCalculationHelperThingy::calculatePreliminaryCropSpecification(
originalDimensions: $originalDimensions,
originalFocalPoint: $originalFocalPoint,
requestedDimensions: $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);

$thumbnail->setResource($processedImageInfo['resource']);
$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);
Expand Down
Loading

0 comments on commit 6bd01c1

Please sign in to comment.