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