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