generated from Nodarx/template
b01b4bdbd2
Implements the shared preview provider contract using ImageMagick to generate thumbnails for supported source types.
202 lines
6.3 KiB
PHP
202 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXM\ProviderPreviewImageMagick;
|
|
|
|
use Imagick;
|
|
use ImagickException;
|
|
use KTXF\Preview\MimeType;
|
|
use KTXF\Preview\PreviewGenerationException;
|
|
use KTXF\Preview\PreviewRequest;
|
|
use KTXF\Preview\PreviewResult;
|
|
use KTXF\Preview\PreviewSource;
|
|
use KTXF\Preview\Provider\PreviewProviderInterface;
|
|
use KTXF\Resource\BinaryResource;
|
|
use KTXF\Resource\Provider\ProviderInterface;
|
|
use Throwable;
|
|
|
|
final class Provider implements PreviewProviderInterface
|
|
{
|
|
private const INPUT_FORMATS = [
|
|
'JPEG' => 'image/jpeg',
|
|
'PNG' => 'image/png',
|
|
'WEBP' => 'image/webp',
|
|
'GIF' => 'image/gif',
|
|
];
|
|
|
|
private const OUTPUT_FORMATS = [
|
|
'image/jpeg' => 'JPEG',
|
|
'image/png' => 'PNG',
|
|
'image/webp' => 'WEBP',
|
|
];
|
|
|
|
public function type(): string
|
|
{
|
|
return ProviderInterface::TYPE_PREVIEW;
|
|
}
|
|
|
|
public function identifier(): string
|
|
{
|
|
return 'imagemagick';
|
|
}
|
|
|
|
public function label(): string
|
|
{
|
|
return 'ImageMagick';
|
|
}
|
|
|
|
public function description(): string
|
|
{
|
|
return 'Generates previews for common image formats';
|
|
}
|
|
|
|
public function icon(): string
|
|
{
|
|
return 'image';
|
|
}
|
|
|
|
public function supportedMimeTypes(): array
|
|
{
|
|
return array_values(self::INPUT_FORMATS);
|
|
}
|
|
|
|
public function outputMimeTypes(): array
|
|
{
|
|
return array_keys(self::OUTPUT_FORMATS);
|
|
}
|
|
|
|
public function supports(string $sourceMimeType, PreviewRequest $request): bool
|
|
{
|
|
try {
|
|
$sourceMimeType = MimeType::normalize($sourceMimeType);
|
|
} catch (Throwable) {
|
|
return false;
|
|
}
|
|
|
|
return in_array($sourceMimeType, $this->supportedMimeTypes(), true)
|
|
&& isset(self::OUTPUT_FORMATS[$request->preferredMimeType])
|
|
&& ($request->page === null || $request->page === 1)
|
|
&& $request->timestamp === null;
|
|
}
|
|
|
|
public function generate(PreviewSource $source, PreviewRequest $request): PreviewResult
|
|
{
|
|
if (!$this->supports($source->mimeType, $request)) {
|
|
throw new PreviewGenerationException('ImageMagick does not support the requested preview conversion');
|
|
}
|
|
if ($source->size !== null && $source->size > $request->maxSourceSize) {
|
|
throw new PreviewGenerationException('Preview source exceeds the configured size limit');
|
|
}
|
|
|
|
$images = new Imagick();
|
|
$image = null;
|
|
try {
|
|
$images->readImageBlob($this->readSource($source, $request->maxSourceSize));
|
|
$images->setIteratorIndex(0);
|
|
$image = $images->getImage();
|
|
|
|
$actualMimeType = self::INPUT_FORMATS[strtoupper($image->getImageFormat())] ?? null;
|
|
if ($actualMimeType === null || $actualMimeType !== $source->mimeType) {
|
|
throw new PreviewGenerationException('Decoded image type does not match the source MIME type');
|
|
}
|
|
|
|
$image->autoOrient();
|
|
$this->resize($image, $request->maxWidth, $request->maxHeight);
|
|
$image->stripImage();
|
|
$image->setImagePage(0, 0, 0, 0);
|
|
$this->encode($image, $request->preferredMimeType, $request->quality);
|
|
|
|
$content = $image->getImageBlob();
|
|
if ($content === '') {
|
|
throw new PreviewGenerationException('ImageMagick produced an empty preview');
|
|
}
|
|
|
|
$mimeType = $request->preferredMimeType;
|
|
return new PreviewResult(
|
|
new BinaryResource(
|
|
'preview.' . $this->extension($mimeType),
|
|
$mimeType,
|
|
(static function () use ($content): \Generator { yield $content; })(),
|
|
),
|
|
$image->getImageWidth(),
|
|
$image->getImageHeight(),
|
|
);
|
|
} catch (PreviewGenerationException $exception) {
|
|
throw $exception;
|
|
} catch (ImagickException $exception) {
|
|
throw new PreviewGenerationException('ImageMagick could not generate the preview', previous: $exception);
|
|
} finally {
|
|
$image?->clear();
|
|
$images->clear();
|
|
}
|
|
}
|
|
|
|
public function generatorVersion(): string
|
|
{
|
|
$version = Imagick::getVersion();
|
|
return $version['versionString'] ?? 'ImageMagick';
|
|
}
|
|
|
|
public function priority(): int
|
|
{
|
|
return 100;
|
|
}
|
|
|
|
private function readSource(PreviewSource $source, int $limit): string
|
|
{
|
|
$content = '';
|
|
foreach ($source->open()->stream() as $chunk) {
|
|
if (!is_string($chunk)) {
|
|
throw new PreviewGenerationException('Preview source yielded a non-string chunk');
|
|
}
|
|
if (strlen($content) + strlen($chunk) > $limit) {
|
|
throw new PreviewGenerationException('Preview source exceeds the configured size limit');
|
|
}
|
|
$content .= $chunk;
|
|
}
|
|
if ($content === '') {
|
|
throw new PreviewGenerationException('Preview source is empty');
|
|
}
|
|
return $content;
|
|
}
|
|
|
|
private function resize(Imagick $image, int $maxWidth, int $maxHeight): void
|
|
{
|
|
$width = $image->getImageWidth();
|
|
$height = $image->getImageHeight();
|
|
$scale = min($maxWidth / $width, $maxHeight / $height, 1);
|
|
if ($scale < 1) {
|
|
$image->resizeImage(
|
|
max(1, (int) floor($width * $scale)),
|
|
max(1, (int) floor($height * $scale)),
|
|
Imagick::FILTER_LANCZOS,
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
|
|
private function encode(Imagick $image, string $mimeType, int $quality): void
|
|
{
|
|
if ($mimeType === 'image/jpeg' && $image->getImageAlphaChannel()) {
|
|
$image->setImageBackgroundColor('white');
|
|
$flattened = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
|
|
$image->clear();
|
|
$image->readImageBlob($flattened->getImageBlob());
|
|
$flattened->clear();
|
|
}
|
|
|
|
$image->setImageFormat(self::OUTPUT_FORMATS[$mimeType]);
|
|
$image->setImageCompressionQuality($quality);
|
|
}
|
|
|
|
private function extension(string $mimeType): string
|
|
{
|
|
return match ($mimeType) {
|
|
'image/jpeg' => 'jpg',
|
|
'image/png' => 'png',
|
|
default => 'webp',
|
|
};
|
|
}
|
|
}
|