Skip to content

Commit

Permalink
Added custom image thumbnail stream endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
mcop1 committed Dec 3, 2024
1 parent 4f570df commit 972bd25
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 1 deletion.
118 changes: 118 additions & 0 deletions src/Asset/Controller/Image/CustomStreamController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Asset\Controller\Image;

use OpenApi\Attributes\Get;
use Pimcore\Bundle\StudioBackendBundle\Asset\Attribute\Response\Header\ContentDisposition;
use Pimcore\Bundle\StudioBackendBundle\Asset\MappedParameter\ImageDownloadConfigParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\ContainParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\CoverParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\ForceResizeParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\FrameParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\ImageConfigParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\MimeTypeParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query\ResizeModeParameter;
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\AssetServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Asset\Service\BinaryServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\AccessDeniedException;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidElementTypeException;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ThumbnailResizingFailedException;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\UserNotFoundException;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags;
use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface;
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;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @internal
*/
final class CustomStreamController extends AbstractApiController
{
public function __construct(
private readonly AssetServiceInterface $assetService,
private readonly BinaryServiceInterface $binaryService,
private readonly SecurityServiceInterface $securityService,
SerializerInterface $serializer
) {
parent::__construct($serializer);
}

/**
* @throws AccessDeniedException
* @throws NotFoundException
* @throws InvalidElementTypeException
* @throws ThumbnailResizingFailedException
* @throws UserNotFoundException
*/
#[Route(
'/assets/{id}/image/stream/custom',
name: 'pimcore_studio_api_stream_image_custom',
methods: ['GET']
)]
#[IsGranted(UserPermissions::ASSETS->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);
}
}
95 changes: 94 additions & 1 deletion src/Asset/MappedParameter/ImageDownloadConfigParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
35 changes: 35 additions & 0 deletions src/Asset/OpenApi/Attribute/Parameter/Query/ContainParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query;

use Attribute;
use OpenApi\Attributes\QueryParameter;
use OpenApi\Attributes\Schema;

#[Attribute(Attribute::TARGET_METHOD)]
final class ContainParameter extends QueryParameter
{
public function __construct(string $description = 'Contain', mixed $defaultValue = null)
{
parent::__construct(
name: 'contain',
description: $description,
in: 'query',
schema: new Schema(type: 'boolean', example: $defaultValue),
);
}
}
35 changes: 35 additions & 0 deletions src/Asset/OpenApi/Attribute/Parameter/Query/CoverParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query;

use Attribute;
use OpenApi\Attributes\QueryParameter;
use OpenApi\Attributes\Schema;

#[Attribute(Attribute::TARGET_METHOD)]
final class CoverParameter extends QueryParameter
{
public function __construct(string $description = 'Cover', mixed $defaultValue = null)
{
parent::__construct(
name: 'cover',
description: $description,
in: 'query',
schema: new Schema(type: 'boolean', example: $defaultValue),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Asset\OpenApi\Attribute\Parameter\Query;

use Attribute;
use OpenApi\Attributes\QueryParameter;
use OpenApi\Attributes\Schema;

#[Attribute(Attribute::TARGET_METHOD)]
final class ForceResizeParameter extends QueryParameter
{
public function __construct(string $description = 'ForceResize', mixed $defaultValue = null)
{
parent::__construct(
name: 'forceResize',
description: $description,
in: 'query',
schema: new Schema(type: 'boolean', example: $defaultValue),
);
}
}
16 changes: 16 additions & 0 deletions src/Asset/Service/BinaryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\Element\Service\StorageServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementProcessingNotCompletedException;
Expand Down Expand Up @@ -79,6 +80,21 @@ public function streamPreviewImageThumbnail(Asset $image): StreamedResponse
);
}

public function streamImageThumbnailFromConfig(
Asset $image,
ImageDownloadConfigParameter $configParameter
): StreamedResponse
{
if (!$image instanceof Image) {
throw new InvalidElementTypeException($image->getType());
}

return $this->getStreamedResponse(
$this->thumbnailService->getThumbnailFromConfiguration($image, $configParameter),
HttpResponseHeaders::INLINE_TYPE->value
);
}

/**
* @throws ElementProcessingNotCompletedException
* @throws InvalidElementTypeException
Expand Down
9 changes: 9 additions & 0 deletions src/Asset/Service/BinaryServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 972bd25

Please sign in to comment.