Skip to content

Add imglab integration #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Classes/ImageService/ImageServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImagorUriSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixFolderSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixProxySource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabProxySource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabUriSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabWebSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyFileSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyUriSource;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\OptimoleUriSource;
Expand Down Expand Up @@ -52,6 +55,7 @@ public function __invoke(): ?ImageServiceInterface
CloudinaryImageService::getIdentifier() => $this->getCloudinaryImageService($options),
CloudImageImageService::getIdentifier() => $this->getCloudImageImageService($options),
GumletImageService::getIdentifier() => $this->getGumletImageService($options),
ImglabImageService::getIdentifier() => $this->getImglabImageService($options),
};
}

Expand Down Expand Up @@ -236,4 +240,23 @@ private function getGumletImageService(array $options): GumletImageService
$options,
);
}

private function getImglabImageService(array $options): ImglabImageService
{
$source = match ($options['source_loader']) {
ImglabWebSource::IDENTIFIER => (static function () use ($options): ImglabWebSource {
return new ImglabWebSource();
})(),
ImglabProxySource::IDENTIFIER => (static function () use ($options): ImglabProxySource {
return new ImglabProxySource(
$options['source_uri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'),
);
})(),
};

return new ImglabImageService(
$source,
$options,
);
}
}
137 changes: 137 additions & 0 deletions Classes/ImageService/ImglabImageService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

namespace SomehowDigital\Typo3\MediaProcessing\ImageService;

use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabUri;
use SomehowDigital\Typo3\MediaProcessing\UriBuilder\UriSourceInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use TYPO3\CMS\Core\Imaging\ImageDimension;
use TYPO3\CMS\Core\Resource\Processing\TaskInterface;

class ImglabImageService extends ImageServiceAbstract
{
public static function getIdentifier(): string
{
return 'imglab';
}

public function __construct(
protected readonly UriSourceInterface $source,
protected array $options,
) {
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$this->options = $resolver->resolve($options);
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'api_endpoint' => null,
'source_loader' => 'web',
'source_uri' => null,
'signature' => false,
'signature_key' => null,
'signature_salt' => null,
]);
}

public function getEndpoint(): string
{
return $this->options['api_endpoint'];
}

public function hasSignature(): bool
{
return (bool) $this->options['signature'];
}

public function getSignatureKey(): ?string
{
return $this->options['signature_key'] ?: null;
}

public function getSignatureSalt(): ?string
{
return $this->options['signature_salt'] ?: null;
}

public function hasConfiguration(): bool
{
return filter_var($this->getEndpoint(), FILTER_VALIDATE_URL) !== false;
}

public function getSupportedMimeTypes(): array
{
return [
'image/jpeg',
'image/jp2',
'image/jpx',
'image/jpm',
'image/png',
'image/gif',
'image/webp',
'image/heic',
'application/pdf',
'video/youtube',
'video/vimeo',
];
}

public function canProcessTask(TaskInterface $task): bool
{
return
in_array($task->getName(), ['Preview', 'CropScaleMask'], true) &&
in_array($task->getSourceFile()->getMimeType(), $this->getSupportedMimeTypes(), true);
}

public function processTask(TaskInterface $task): ImageServiceResultInterface
{
$file = $task->getSourceFile();
$configuration = $task->getTargetFile()->getProcessingConfiguration();
$dimension = ImageDimension::fromProcessingTask($task);

$uri = new ImglabUri(
$this->getEndpoint(),
$this->hasSignature() ? $this->getSignatureKey() : null,
$this->hasSignature() ? $this->getSignatureSalt() : null,
);

$uri->setSource($this->source->getSource($file));

$mode = (static function ($configuration) {
switch (true) {
default:
return 'clip';

case str_ends_with((string) ($configuration['width'] ?? ''), 'c'):
case str_ends_with((string) ($configuration['height'] ?? ''), 'c'):
return 'crop';
}
})($configuration);

$uri->setMode($mode);

if (isset($configuration['crop'])) {
$uri->setCrop(
(int) $configuration['crop']->getWidth(),
(int) $configuration['crop']->getHeight(),
(int) $configuration['crop']->getOffsetLeft(),
(int) $configuration['crop']->getOffsetTop(),
);
}

$width = (int) ($configuration['width'] ?? $configuration['maxWidth'] ?? null);
$height = (int) ($configuration['height'] ?? $configuration['maxHeight'] ?? null);

$uri->setWidth($width);
$uri->setHeight($height);

return new ImageServiceResult(
$uri,
$dimension,
);
}
}
41 changes: 41 additions & 0 deletions Classes/UriBuilder/ImglabProxySource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace SomehowDigital\Typo3\MediaProcessing\UriBuilder;

use SomehowDigital\Typo3\MediaProcessing\Utility\OnlineMediaUtility;
use TYPO3\CMS\Core\Resource\FileInterface;

class ImglabProxySource implements UriSourceInterface
{
public const IDENTIFIER = 'proxy';

public function __construct(
private readonly string $host,
) {
}

public function getHost(): string
{
return $this->host;
}

public function getSource(FileInterface $file): string
{
$url = OnlineMediaUtility::getPreviewImage($file) ?? $file->getPublicUrl();

return $this->build($url);
}

private function build(string $url): string
{
$path = parse_url($url, PHP_URL_PATH);
$query = parse_url($url, PHP_URL_QUERY) ?? '';

return strtr('%host%/%path%', [
'%host%' => trim($this->getHost(), '/'),
'%path%' => implode('?', array_filter([trim($path, '/'), trim($query)])),
]);
}
}
168 changes: 168 additions & 0 deletions Classes/UriBuilder/ImglabUri.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

declare(strict_types=1);

namespace SomehowDigital\Typo3\MediaProcessing\UriBuilder;

class ImglabUri implements UriInterface
{
public const SIGNATURE_ALGORITHM = 'sha256';

private ?string $source = null;

private ?string $mode = null;

private ?int $width = null;

private ?int $height = null;

private ?array $crop = null;

public function __construct(
private readonly string $endpoint,
private readonly ?string $key,
private readonly ?string $salt,
) {
}

public function __invoke(): string
{
return $this->build();
}

public function __toString(): string
{
return $this->build();
}

public function getEndpoint(): string
{
return $this->endpoint;
}

public function getKey(): ?string
{
return $this->key;
}

public function getSalt(): ?string
{
return $this->salt;
}

public function setSource(string $source): self
{
$this->source = $source;

return $this;
}

public function getSource(): ?string
{
return $this->source;
}

public function setMode(string $mode): self
{
$this->mode = $mode;

return $this;
}

public function getMode(): ?string
{
return $this->mode;
}

public function setWidth(int $width): self
{
$this->width = $width;

return $this;
}

public function getWidth(): ?int
{
return $this->width;
}

public function setHeight(int $height): self
{
$this->height = $height;

return $this;
}

public function getHeight(): ?int
{
return $this->height;
}

public function setCrop(int $width, int $height, int $horizontal, int $vertical): self
{
$this->crop = [
$width,
$height,
$horizontal,
$vertical,
];

return $this;
}

public function getCrop(): ?array
{
return $this->crop;
}

private function build(): string
{
$path = $this->buildPath();

$signature = $this->getKey()
? $this->calculateSignature($path)
: null;

return strtr($signature ? '%endpoint%/%path%&signature=%signature%' : '%endpoint%/%path%', array_filter([
'%endpoint%' => trim($this->endpoint, '/'),
'%path%' => $path,
'%signature%' => $signature,
]));
}

private function buildPath(): string
{
$parameters = array_filter([
'mode' => $this->getMode(),
'width' => $this->getWidth(),
'height' => $this->getHeight(),
]);

$options = implode('&', array_map(static function ($name, $value) {
return strtr('%name%=%value%', [
'%name%' => $name,
'%value%' => $value,
]);
}, array_keys($parameters), $parameters));

$source = rawurlencode(trim($this->getSource(), '/'));

return strtr('%source%?%options%', [
'%source%' => $source,
'%options%' => $options,
]);
}

private function calculateSignature(string $path): string
{
$data = strtr('%salt%/%path%', [
'%salt%' => base64_decode($this->getSalt(), true),
'%path%' => rawurldecode($path),
]);

$hash = hash_hmac(static::SIGNATURE_ALGORITHM, $data, base64_decode($this->getKey(), true), true);
$digest = base64_encode($hash);

return rtrim(strtr($digest, '+/', '-_'), '=');
}
}
Loading