From 972bd256717a29d82c44112732eaef03393f3555 Mon Sep 17 00:00:00 2001 From: Marco Perberschlager Date: Tue, 3 Dec 2024 16:17:06 +0100 Subject: [PATCH 1/5] Added custom image thumbnail stream endpoint --- .../Image/CustomStreamController.php | 118 ++++++++++++++++++ .../ImageDownloadConfigParameter.php | 95 +++++++++++++- .../Parameter/Query/ContainParameter.php | 35 ++++++ .../Parameter/Query/CoverParameter.php | 35 ++++++ .../Parameter/Query/ForceResizeParameter.php | 35 ++++++ src/Asset/Service/BinaryService.php | 16 +++ src/Asset/Service/BinaryServiceInterface.php | 9 ++ src/Asset/Service/ThumbnailService.php | 12 ++ translations/studio_api_docs.en.yaml | 5 + 9 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/Asset/Controller/Image/CustomStreamController.php create mode 100644 src/Asset/OpenApi/Attribute/Parameter/Query/ContainParameter.php create mode 100644 src/Asset/OpenApi/Attribute/Parameter/Query/CoverParameter.php create mode 100644 src/Asset/OpenApi/Attribute/Parameter/Query/ForceResizeParameter.php diff --git a/src/Asset/Controller/Image/CustomStreamController.php b/src/Asset/Controller/Image/CustomStreamController.php new file mode 100644 index 000000000..792ccc832 --- /dev/null +++ b/src/Asset/Controller/Image/CustomStreamController.php @@ -0,0 +1,118 @@ +value)] + #[Get( + path: self::PREFIX . '/assets/{id}/image/stream/custom', + operationId: 'asset_image_stream_custom', + description: 'asset_image_stream_custom_description', + summary: 'asset_image_stream_custom_summary', + tags: [Tags::Assets->name] + )] + #[IdParameter(type: 'image')] + #[MimeTypeParameter] + #[ResizeModeParameter] + #[ImageConfigParameter('width', 140)] + #[ImageConfigParameter('height')] + #[ImageConfigParameter('quality')] + #[ImageConfigParameter('dpi')] + #[ContainParameter] + #[FrameParameter] + #[CoverParameter] + #[ForceResizeParameter] + #[SuccessResponse( + description: 'asset_image_stream_custom_success_response', + content: [new MediaType('image/*')], + headers: [new ContentDisposition(HttpResponseHeaders::INLINE_TYPE->value)] + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::NOT_FOUND, + ])] + public function streamCustomThumbnailConfig( + int $id, + #[MapQueryString] ImageDownloadConfigParameter $parameters + ): StreamedResponse { + $asset = $this->assetService->getAssetElement( + $this->securityService->getCurrentUser(), + $id + ); + + return $this->binaryService->streamImageThumbnailFromConfig($asset, $parameters); + } +} diff --git a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php index 27ac702cc..9fd13faa0 100644 --- a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php +++ b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php @@ -32,7 +32,12 @@ public function __construct( private ?int $width = null, private ?int $height = null, private ?int $quality = null, - private ?int $dpi = null + private ?int $dpi = null, + private ?string $positioning = 'center', + private string $cover = 'false', + private string $frame = 'false', + private string $contain = 'false', + private string $forceResize = 'false', ) { if (!in_array($this->mimeType, [MimeTypes::JPEG->value, MimeTypes::PNG->value], true)) { throw new InvalidArgumentException('Invalid mime type' . $this->mimeType); @@ -41,6 +46,28 @@ public function __construct( if (!in_array($this->resizeMode, ResizeModes::ALLOWED_MODES)) { throw new InvalidArgumentException('Invalid resize mode ' . $this->resizeMode); } + + if($this->resizeMode === ResizeModes::SCALE_BY_HEIGHT && !$this->isValidHeight()) { + throw new InvalidArgumentException( + 'Height must be set and non-negative when using scale by width resize mode' + ); + } + + if($this->resizeMode === ResizeModes::SCALE_BY_WIDTH && !$this->isValidWidth()) { + throw new InvalidArgumentException( + 'Width must be set and non-negative when using scale by width resize mode' + ); + } + + if( + (!$this->isValidWidth() || !$this->isValidHeight()) && + ($this->hasFrame() || $this->hasCover() || $this->hasContain() || $this->resizeMode === ResizeModes::RESIZE) + ) + { + throw new InvalidArgumentException( + 'Width, height must be set and non-negative when using frame, cover, contain or resize' + ); + } } public function getMimeType(): string @@ -72,4 +99,70 @@ public function getDpi(): ?int { return $this->dpi; } + + public function getCoverTransformation(): array + { + return [ + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'forceResize' => $this->getForceResize(), + 'positioning' => $this->getPositioning(), + ]; + } + + public function getFrameTransformation(): array + { + return [ + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'forceResize' => $this->getForceResize() + ]; + } + + public function getContainTransformation(): array + { + return [ + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'forceResize' => $this->getForceResize() + ]; + } + + public function getPositioning(): ?string + { + return $this->positioning; + } + + public function getForceResize(): bool + { + return $this->forceResize === 'true'; // TODO: symfony 7.1 will support bool type + } + + public function hasCover(): bool + { + // TODO: symfony 7.1 will support bool type + return $this->cover === 'true'; + } + + public function hasFrame(): bool + { + // TODO: symfony 7.1 will support bool type + return $this->frame === 'true'; + } + + public function hasContain(): bool + { + // TODO: symfony 7.1 will support bool type + return $this->contain === 'true'; + } + + private function isValidWidth(): bool + { + return $this->width !== null && $this->width > 0; + } + + private function isValidHeight(): bool + { + return $this->height !== null && $this->height > 0; + } } diff --git a/src/Asset/OpenApi/Attribute/Parameter/Query/ContainParameter.php b/src/Asset/OpenApi/Attribute/Parameter/Query/ContainParameter.php new file mode 100644 index 000000000..5ea3263a1 --- /dev/null +++ b/src/Asset/OpenApi/Attribute/Parameter/Query/ContainParameter.php @@ -0,0 +1,35 @@ +getType()); + } + + return $this->getStreamedResponse( + $this->thumbnailService->getThumbnailFromConfiguration($image, $configParameter), + HttpResponseHeaders::INLINE_TYPE->value + ); + } + /** * @throws ElementProcessingNotCompletedException * @throws InvalidElementTypeException diff --git a/src/Asset/Service/BinaryServiceInterface.php b/src/Asset/Service/BinaryServiceInterface.php index 37dbb819a..c3143f88b 100644 --- a/src/Asset/Service/BinaryServiceInterface.php +++ b/src/Asset/Service/BinaryServiceInterface.php @@ -17,6 +17,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\Asset\Service; use League\Flysystem\FilesystemException; +use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ImageDownloadConfigParameter; use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\VideoImageStreamConfigParameter; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementProcessingNotCompletedException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementStreamResourceNotFoundException; @@ -47,6 +48,14 @@ public function downloadVideoByThumbnail( */ public function streamPreviewImageThumbnail(Asset $image): StreamedResponse; + /** + * @throws InvalidElementTypeException|InvalidThumbnailException + */ + public function streamImageThumbnailFromConfig( + Asset $image, + ImageDownloadConfigParameter $configParameter + ): StreamedResponse; + /** * @throws ElementProcessingNotCompletedException * @throws InvalidElementTypeException diff --git a/src/Asset/Service/ThumbnailService.php b/src/Asset/Service/ThumbnailService.php index cbf4cbb5c..25eae6a78 100644 --- a/src/Asset/Service/ThumbnailService.php +++ b/src/Asset/Service/ThumbnailService.php @@ -168,6 +168,18 @@ private function getImageThumbnailConfig( } } + if($parameters->hasCover()) { + $thumbnailConfig->addItem('cover', $parameters->getCoverTransformation()); + } + + if($parameters->hasFrame()) { + $thumbnailConfig->addItem('frame', $parameters->getFrameTransformation()); + } + + if($parameters->hasContain()) { + $thumbnailConfig->addItem('contain', $parameters->getContainTransformation()); + } + return $thumbnailConfig; } diff --git a/translations/studio_api_docs.en.yaml b/translations/studio_api_docs.en.yaml index f30bde537..0f02d4766 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -115,6 +115,11 @@ asset_image_download_custom_description: | The {id} must be an ID of existing asset image asset_image_download_custom_success_response: Custom image binary file asset_image_download_custom_summary: Download custom image by ID and configuration +asset_image_stream_custom_summary: Stream custom image thumbnail by ID and configuration +asset_image_stream_custom_description: | + Stream image asset thumbnail based on the provided {id} and configuration parameters.
+ The {id} must be an ID of existing asset image +asset_image_stream_custom_success_response: Image asset stream based on custom thumbnail configuration asset_image_stream_preview_description: | Stream image asset preview based on the provided {id}.
The {id} must be an ID of existing asset image From b4c275d1004a6ca52d9c67a91ac75bd0c1c1b2a0 Mon Sep 17 00:00:00 2001 From: mcop1 Date: Tue, 3 Dec 2024 15:21:21 +0000 Subject: [PATCH 2/5] Apply php-cs-fixer changes --- .../Controller/Image/CustomStreamController.php | 1 - .../ImageDownloadConfigParameter.php | 13 ++++++------- src/Asset/Service/BinaryService.php | 3 +-- src/Asset/Service/ThumbnailService.php | 6 +++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Asset/Controller/Image/CustomStreamController.php b/src/Asset/Controller/Image/CustomStreamController.php index 792ccc832..e20acf30a 100644 --- a/src/Asset/Controller/Image/CustomStreamController.php +++ b/src/Asset/Controller/Image/CustomStreamController.php @@ -43,7 +43,6 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\Routing\Attribute\Route; diff --git a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php index 9fd13faa0..c8aaf66e5 100644 --- a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php +++ b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php @@ -47,23 +47,22 @@ public function __construct( throw new InvalidArgumentException('Invalid resize mode ' . $this->resizeMode); } - if($this->resizeMode === ResizeModes::SCALE_BY_HEIGHT && !$this->isValidHeight()) { + if ($this->resizeMode === ResizeModes::SCALE_BY_HEIGHT && !$this->isValidHeight()) { throw new InvalidArgumentException( 'Height must be set and non-negative when using scale by width resize mode' ); } - if($this->resizeMode === ResizeModes::SCALE_BY_WIDTH && !$this->isValidWidth()) { + if ($this->resizeMode === ResizeModes::SCALE_BY_WIDTH && !$this->isValidWidth()) { throw new InvalidArgumentException( 'Width must be set and non-negative when using scale by width resize mode' ); } - if( + if ( (!$this->isValidWidth() || !$this->isValidHeight()) && ($this->hasFrame() || $this->hasCover() || $this->hasContain() || $this->resizeMode === ResizeModes::RESIZE) - ) - { + ) { throw new InvalidArgumentException( 'Width, height must be set and non-negative when using frame, cover, contain or resize' ); @@ -115,7 +114,7 @@ public function getFrameTransformation(): array return [ 'width' => $this->getWidth(), 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize() + 'forceResize' => $this->getForceResize(), ]; } @@ -124,7 +123,7 @@ public function getContainTransformation(): array return [ 'width' => $this->getWidth(), 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize() + 'forceResize' => $this->getForceResize(), ]; } diff --git a/src/Asset/Service/BinaryService.php b/src/Asset/Service/BinaryService.php index 1173a8f2b..333bc55d2 100644 --- a/src/Asset/Service/BinaryService.php +++ b/src/Asset/Service/BinaryService.php @@ -83,8 +83,7 @@ public function streamPreviewImageThumbnail(Asset $image): StreamedResponse public function streamImageThumbnailFromConfig( Asset $image, ImageDownloadConfigParameter $configParameter - ): StreamedResponse - { + ): StreamedResponse { if (!$image instanceof Image) { throw new InvalidElementTypeException($image->getType()); } diff --git a/src/Asset/Service/ThumbnailService.php b/src/Asset/Service/ThumbnailService.php index 25eae6a78..7eb514fa6 100644 --- a/src/Asset/Service/ThumbnailService.php +++ b/src/Asset/Service/ThumbnailService.php @@ -168,15 +168,15 @@ private function getImageThumbnailConfig( } } - if($parameters->hasCover()) { + if ($parameters->hasCover()) { $thumbnailConfig->addItem('cover', $parameters->getCoverTransformation()); } - if($parameters->hasFrame()) { + if ($parameters->hasFrame()) { $thumbnailConfig->addItem('frame', $parameters->getFrameTransformation()); } - if($parameters->hasContain()) { + if ($parameters->hasContain()) { $thumbnailConfig->addItem('contain', $parameters->getContainTransformation()); } From 2ea898b666cd67102689bf5fb5c17cd21b3cb6f1 Mon Sep 17 00:00:00 2001 From: Marco Perberschlager Date: Tue, 3 Dec 2024 16:40:23 +0100 Subject: [PATCH 3/5] Extracted duplicated code into local method --- .../ImageDownloadConfigParameter.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php index 9fd13faa0..ff33c0adb 100644 --- a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php +++ b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php @@ -102,30 +102,21 @@ public function getDpi(): ?int public function getCoverTransformation(): array { - return [ - 'width' => $this->getWidth(), - 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize(), + return + [ + ... $this->getBaseTransformationValues(), 'positioning' => $this->getPositioning(), ]; } public function getFrameTransformation(): array { - return [ - 'width' => $this->getWidth(), - 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize() - ]; + return $this->getBaseTransformationValues(); } public function getContainTransformation(): array { - return [ - 'width' => $this->getWidth(), - 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize() - ]; + return $this->getBaseTransformationValues(); } public function getPositioning(): ?string @@ -165,4 +156,13 @@ private function isValidHeight(): bool { return $this->height !== null && $this->height > 0; } + + private function getBaseTransformationValues(): array + { + return [ + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + 'forceResize' => $this->getForceResize() + ]; + } } From 2698f1207dabd4b0b7906b9d0a7de5ec12066133 Mon Sep 17 00:00:00 2001 From: mcop1 Date: Tue, 3 Dec 2024 15:42:15 +0000 Subject: [PATCH 4/5] Apply php-cs-fixer changes --- src/Asset/MappedParameter/ImageDownloadConfigParameter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php index f34c5cfe6..d623e7c79 100644 --- a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php +++ b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php @@ -161,7 +161,7 @@ private function getBaseTransformationValues(): array return [ 'width' => $this->getWidth(), 'height' => $this->getHeight(), - 'forceResize' => $this->getForceResize() + 'forceResize' => $this->getForceResize(), ]; } } From ce22224e6b546df419076b1c9858d1b4bf044772 Mon Sep 17 00:00:00 2001 From: Marco Perberschlager Date: Wed, 4 Dec 2024 09:39:00 +0100 Subject: [PATCH 5/5] Updated formatting ;-) --- src/Asset/MappedParameter/ImageDownloadConfigParameter.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php index f34c5cfe6..2829f3277 100644 --- a/src/Asset/MappedParameter/ImageDownloadConfigParameter.php +++ b/src/Asset/MappedParameter/ImageDownloadConfigParameter.php @@ -101,8 +101,7 @@ public function getDpi(): ?int public function getCoverTransformation(): array { - return - [ + return [ ... $this->getBaseTransformationValues(), 'positioning' => $this->getPositioning(), ];