Skip to content

Commit 9b445d2

Browse files
committed
Task: add svg handling and (possible) cropping
ISSUE: #5652
1 parent 12d32b4 commit 9b445d2

File tree

5 files changed

+192
-14
lines changed

5 files changed

+192
-14
lines changed

Neos.Media/Classes/Command/MediaCommandController.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,20 @@
2222
use Neos\Flow\Cli\CommandController;
2323
use Neos\Flow\Cli\Exception\StopCommandException;
2424
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
25+
use Neos\Flow\Persistence\Exception\InvalidQueryException;
26+
use Neos\Flow\Persistence\Exception\UnknownObjectException;
2527
use Neos\Flow\Persistence\PersistenceManagerInterface;
2628
use Neos\Flow\Reflection\ReflectionService;
2729
use Neos\Flow\ResourceManagement\PersistentResource;
2830
use Neos\Media\Domain\Model\Asset;
2931
use Neos\Media\Domain\Model\AssetCollection;
3032
use Neos\Media\Domain\Model\AssetInterface;
3133
use Neos\Media\Domain\Model\AssetSource\AssetSourceAwareInterface;
34+
use Neos\Media\Domain\Model\Image;
3235
use Neos\Media\Domain\Model\Tag;
3336
use Neos\Media\Domain\Model\VariantSupportInterface;
3437
use Neos\Media\Domain\Repository\AssetRepository;
38+
use Neos\Media\Domain\Repository\ImageRepository;
3539
use Neos\Media\Domain\Repository\ImageVariantRepository;
3640
use Neos\Media\Domain\Repository\ThumbnailRepository;
3741
use Neos\Media\Domain\Service\AssetService;
@@ -40,6 +44,7 @@
4044
use Neos\Media\Domain\Strategy\AssetModelMappingStrategyInterface;
4145
use Neos\Media\Exception\AssetServiceException;
4246
use Neos\Media\Exception\AssetVariantGeneratorException;
47+
use Neos\Media\Exception\ImageFileException;
4348
use Neos\Media\Exception\ThumbnailServiceException;
4449
use Neos\Utility\Arrays;
4550
use Neos\Utility\Files;
@@ -118,6 +123,9 @@ class MediaCommandController extends CommandController
118123
*/
119124
protected $assetVariantGenerator;
120125

126+
#[Flow\Inject]
127+
protected ImageRepository $imageRepository;
128+
121129
/**
122130
* Import resources to asset management
123131
*
@@ -593,6 +601,54 @@ public function renderVariantsCommand(?int $limit = null, bool $quiet = false, b
593601
!$quiet && $this->outputLine($resultMessage ?? sprintf('Generated %u variants', $generatedVariants));
594602
}
595603

604+
/**
605+
* Calculate missing dimensions for SVG assets
606+
*
607+
* @param bool $force update all svg asset dimensions
608+
* @throws InvalidQueryException
609+
* @throws ImageFileException
610+
* @throws UnknownObjectException
611+
*/
612+
public function refreshSvgDimensionsCommand(bool $force = false): void
613+
{
614+
$this->outputLine('Looking for SVG Assets without dimensions');
615+
616+
$queryResult = $this->imageRepository->findAll();
617+
$totalCount = $queryResult->count();
618+
$this->output->progressStart($totalCount);
619+
$updatedCount = 0;
620+
621+
/** @var Image $image */
622+
foreach ($queryResult as $image) {
623+
$this->output->progressAdvance(1);
624+
625+
if (!$this->isSvgImage($image)) {
626+
continue;
627+
}
628+
629+
if ($force || $this->hasMissingDimensions($image)) {
630+
$updatedCount++;
631+
$image->refresh();
632+
// the image repository tries magic we need to circumvent
633+
$this->persistenceManager->update($image);
634+
}
635+
}
636+
637+
$this->output->progressFinish();
638+
$this->outputLine();
639+
$this->outputLine('Added dimensions to %s SVG Assets', [$updatedCount]);
640+
}
641+
642+
private function isSvgImage(Image $image): bool
643+
{
644+
return $image->getResource()->getMediaType() === 'image/svg+xml';
645+
}
646+
647+
private function hasMissingDimensions(Image $image): bool
648+
{
649+
return $image->getWidth() === 0 || $image->getHeight() === 0;
650+
}
651+
596652
/**
597653
* Used as a callback when iterating large results sets
598654
*/

Neos.Media/Classes/Domain/Model/Adjustment/CropImageAdjustment.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* source code.
1414
*/
1515

16+
use Contao\ImagineSvg\Image as ContaoSvgImage;
17+
use Contao\ImagineSvg\SvgBox;
1618
use Doctrine\ORM\Mapping as ORM;
1719
use Imagine\Image\Box;
1820
use Imagine\Image\ImageInterface as ImagineImageInterface;
@@ -216,6 +218,17 @@ public static function calculateDimensionsByAspectRatio(int $originalWidth, int
216218
return [$newX, $newY, $newWidth, $newHeight];
217219
}
218220

221+
public static function createFromCropImageAdjustment(CropImageAdjustment $cropImageAdjustment): self
222+
{
223+
$newAdjustment = new self();
224+
$newAdjustment->setWidth($cropImageAdjustment->getWidth());
225+
$newAdjustment->setHeight($cropImageAdjustment->getHeight());
226+
$newAdjustment->setX($cropImageAdjustment->getX());
227+
$newAdjustment->setY($cropImageAdjustment->getY());
228+
$newAdjustment->setAspectRatio($cropImageAdjustment->getAspectRatio());
229+
return $newAdjustment;
230+
}
231+
219232
/**
220233
* Check if this Adjustment can or should be applied to its ImageVariant.
221234
*
@@ -255,14 +268,22 @@ public function applyToImage(ImagineImageInterface $image): ImagineImageInterfac
255268
[$newX, $newY, $newWidth, $newHeight] = self::calculateDimensionsByAspectRatio($originalWidth, $originalHeight, $desiredAspectRatio);
256269

257270
$point = new Point($newX, $newY);
258-
$box = new Box($newWidth, $newHeight);
271+
$box = $this->createBox($image, $newWidth, $newHeight);
259272
} else {
260273
$point = new Point($this->x, $this->y);
261-
$box = new Box($this->width, $this->height);
274+
$box = $this->createBox($image, $this->width, $this->height);
262275
}
263276
return $image->crop($point, $box);
264277
}
265278

279+
private function createBox(ImagineImageInterface $image, int $width, int $height): Box|SvgBox
280+
{
281+
if ($image instanceof ContaoSvgImage) {
282+
return new SvgBox($width, $height);
283+
}
284+
return new Box($width, $height);
285+
}
286+
266287
/**
267288
* Refits the crop proportions to be the maximum size within the image boundaries.
268289
*

Neos.Media/Classes/Domain/Model/Adjustment/ResizeImageAdjustment.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,20 @@ public function applyToImage(ImagineImageInterface $image)
305305
return $this->resize($image, $this->ratioMode, $this->filter);
306306
}
307307

308+
public static function createFromResizeImageAdjustment(ResizeImageAdjustment $resizeImageAdjustment): self
309+
{
310+
$newAdjustment = new self();
311+
$newAdjustment->setWidth($resizeImageAdjustment->getWidth());
312+
$newAdjustment->setHeight($resizeImageAdjustment->getHeight());
313+
$newAdjustment->setMaximumWidth($resizeImageAdjustment->getMaximumWidth());
314+
$newAdjustment->setMaximumHeight($resizeImageAdjustment->getMaximumHeight());
315+
$newAdjustment->setMinimumWidth($resizeImageAdjustment->getMinimumWidth());
316+
$newAdjustment->setMinimumHeight($resizeImageAdjustment->getMinimumHeight());
317+
$newAdjustment->setRatioMode($resizeImageAdjustment->getRatioMode());
318+
$newAdjustment->setAllowUpScaling(true);
319+
return $newAdjustment;
320+
}
321+
308322
/**
309323
* Calculates and returns the dimensions the image should have according all parameters set
310324
* in this adjustment.

Neos.Media/Classes/Domain/Service/ImageService.php

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* source code.
1212
*/
1313

14+
use Contao\ImagineSvg\Imagine as SvgImagine;
1415
use Imagine\Image\ImageInterface;
1516
use Imagine\Image\ImagineInterface;
1617
use Imagine\Image\Palette\CMYK;
@@ -21,7 +22,9 @@
2122
use Neos\Flow\ResourceManagement\ResourceManager;
2223
use Neos\Flow\Utility\Algorithms;
2324
use Neos\Flow\Utility\Environment;
25+
use Neos\Media\Domain\Model\Adjustment\CropImageAdjustment;
2426
use Neos\Media\Domain\Model\Adjustment\QualityImageAdjustment;
27+
use Neos\Media\Domain\Model\Adjustment\ResizeImageAdjustment;
2528
use Neos\Media\Domain\Repository\AssetRepository;
2629
use Neos\Media\Imagine\Box;
2730
use Neos\Flow\Annotations as Flow;
@@ -103,17 +106,8 @@ public function processImage(PersistentResource $originalResource, array $adjust
103106
$additionalOptions = [];
104107
$adjustmentsApplied = false;
105108

106-
// TODO: Special handling for SVG should be refactored at a later point.
107109
if ($originalResource->getMediaType() === 'image/svg+xml') {
108-
$originalResourceStream = $originalResource->getStream();
109-
$resource = $this->resourceManager->importResource($originalResourceStream, $originalResource->getCollectionName());
110-
fclose($originalResourceStream);
111-
$resource->setFilename($originalResource->getFilename());
112-
return [
113-
'width' => null,
114-
'height' => null,
115-
'resource' => $resource
116-
];
110+
return $this->processSvgImage($originalResource, $adjustments);
117111
}
118112

119113
$resourceUri = $originalResource->createTemporaryLocalCopy();
@@ -214,6 +208,77 @@ public function processImage(PersistentResource $originalResource, array $adjust
214208
return $result;
215209
}
216210

211+
/**
212+
* Process SVG images with adjustments
213+
*
214+
* @param PersistentResource $originalResource
215+
* @param array<ImageAdjustmentInterface> $adjustments
216+
* @return array{width: int|null, height: int|null, resource: PersistentResource}
217+
* @throws Exception
218+
*/
219+
protected function processSvgImage(PersistentResource $originalResource, array $adjustments): array
220+
{
221+
$originalResourceStream = $originalResource->getStream();
222+
223+
$nonAdjustedResult = [
224+
'width' => null,
225+
'height' => null,
226+
'resource' => $originalResource
227+
];
228+
229+
if (is_bool($originalResourceStream)) {
230+
return $nonAdjustedResult;
231+
}
232+
233+
try {
234+
$svgImage = (new SvgImagine())->read($originalResourceStream);
235+
} catch (\Exception) {
236+
return $nonAdjustedResult;
237+
}
238+
239+
$svgImage = $this->applySvgAdjustments($svgImage, $adjustments);
240+
$size = $svgImage->getSize();
241+
242+
$transformedImageTemporaryPathAndFilename = $this->environment->getPathToTemporaryDirectory()
243+
. 'ProcessedImage-' . Algorithms::generateRandomString(13) . '.svg';
244+
245+
$svgImage->save($transformedImageTemporaryPathAndFilename);
246+
$resource = $this->resourceManager->importResource(
247+
$transformedImageTemporaryPathAndFilename,
248+
$originalResource->getCollectionName()
249+
);
250+
$resource->setFilename($originalResource->getFilename());
251+
unlink($transformedImageTemporaryPathAndFilename);
252+
253+
return [
254+
'width' => $size->getWidth(),
255+
'height' => $size->getHeight(),
256+
'resource' => $resource,
257+
];
258+
}
259+
260+
/**
261+
* Apply supported adjustments to SVG image
262+
*
263+
* @param ImageInterface $svgImage
264+
* @param array<ImageAdjustmentInterface> $adjustments
265+
* @return ImageInterface
266+
*/
267+
protected function applySvgAdjustments(ImageInterface $svgImage, array $adjustments): ImageInterface
268+
{
269+
foreach ($adjustments as $adjustment) {
270+
if ($adjustment instanceof CropImageAdjustment) {
271+
$svgAdjustment = CropImageAdjustment::createFromCropImageAdjustment($adjustment);
272+
$svgImage = $svgAdjustment->applyToImage($svgImage);
273+
} elseif ($adjustment instanceof ResizeImageAdjustment) {
274+
$svgAdjustment = ResizeImageAdjustment::createFromResizeImageAdjustment($adjustment);
275+
$svgImage = $svgAdjustment->applyToImage($svgImage);
276+
}
277+
}
278+
279+
return $svgImage;
280+
}
281+
217282
/**
218283
* @param array $additionalOptions
219284
* @return array
@@ -260,9 +325,8 @@ public function getImageSize(PersistentResource $resource)
260325
return $imageSize;
261326
}
262327

263-
// TODO: Special handling for SVG should be refactored at a later point.
264328
if ($resource->getMediaType() === 'image/svg+xml') {
265-
$imageSize = ['width' => null, 'height' => null];
329+
return $this->getSvgImageSize($resource);
266330
} else {
267331
try {
268332
$imagineImage = $this->imagineService->read($resource->getStream());
@@ -303,6 +367,28 @@ protected function applyAdjustments(ImageInterface $image, array $adjustments, &
303367
return $image;
304368
}
305369

370+
/**
371+
* Get the size of an SVG image
372+
*
373+
* @param PersistentResource $resource
374+
* @return array{width: int|null, height: int|null}
375+
*/
376+
protected function getSvgImageSize(PersistentResource $resource): array
377+
{
378+
try {
379+
$resourceStream = $resource->getStream();
380+
if (is_bool($resourceStream)) {
381+
throw new \Exception('the stream of the given resource is not available');
382+
}
383+
384+
$svgImage = (new SvgImagine())->read($resourceStream);
385+
$size = $svgImage->getSize();
386+
return ['width' => $size->getWidth(), 'height' => $size->getHeight()];
387+
} catch (\Exception) {
388+
return ['width' => null, 'height' => null];
389+
}
390+
}
391+
306392
/**
307393
* Detects whether the given GIF image data contains more than one frame
308394
*

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"enshrined/svg-sanitize": "^0.22.0",
4242
"neos/imagine": "^3.1.0",
4343
"imagine/imagine": "*",
44+
"contao/imagine-svg": "^1.0",
4445
"neos/party": "~7.0.3",
4546
"neos/fusion-form": "^2.1 || ^3.0",
4647
"neos/form": "*",

0 commit comments

Comments
 (0)