chore: complete update
This commit is contained in:
244
src/Domain/Media/ImagickImageProcessor.php
Normal file
244
src/Domain/Media/ImagickImageProcessor.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use Imagick;
|
||||
use ImagickException;
|
||||
use ImagickPixel;
|
||||
|
||||
/**
|
||||
* ImageMagick-basierter Bildprozessor für optimale Bildqualität
|
||||
*/
|
||||
final readonly class ImagickImageProcessor implements ImageProcessorInterface
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createAllVariants(Image $image): array
|
||||
{
|
||||
$variants = [];
|
||||
|
||||
foreach (ImageVariantConfig::getAllVariants() as $variantConfig) {
|
||||
try {
|
||||
$variant = $this->createVariant($image, $variantConfig);
|
||||
$variants[] = $variant;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to create variant {$variantConfig['type']->value}_{$variantConfig['size']->value}_{$variantConfig['format']->value}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $variants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function createVariantsForType(Image $image, ImageVariantType $type): array
|
||||
{
|
||||
$variants = [];
|
||||
|
||||
foreach (ImageVariantConfig::getVariantsForType($type) as $variantConfig) {
|
||||
try {
|
||||
$variant = $this->createVariant($image, $variantConfig);
|
||||
$variants[] = $variant;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to create variant {$variantConfig['type']->value}_{$variantConfig['size']->value}_{$variantConfig['format']->value}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $variants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine einzelne Bildvariante
|
||||
*
|
||||
* @param Image $image
|
||||
* @param array $config
|
||||
* @return ImageVariant
|
||||
*/
|
||||
private function createVariant(Image $image, array $config): ImageVariant
|
||||
{
|
||||
$sourcePath = $image->path . $image->filename;
|
||||
|
||||
/** @var ImageVariantType $type */
|
||||
/** @var ImageSize $size */
|
||||
/** @var ImageFormat $format */
|
||||
$type = $config['type'];
|
||||
$size = $config['size'];
|
||||
$format = $config['format'];
|
||||
$width = $config['width'];
|
||||
|
||||
$filename = $this->generateVariantFilename(
|
||||
$image->filename,
|
||||
$type->value,
|
||||
$size->value,
|
||||
$format->value
|
||||
);
|
||||
|
||||
$destination = $image->path . $filename;
|
||||
|
||||
$actualDimensions = $this->processImage($sourcePath, $destination, $width, $format);
|
||||
|
||||
$fileSize = filesize($destination);
|
||||
|
||||
return new ImageVariant(
|
||||
imageId: $image->ulid,
|
||||
variantType: $type->value,
|
||||
size: $size->value,
|
||||
format: $format->value,
|
||||
mimeType: $format->getMimeType(),
|
||||
fileSize: $fileSize,
|
||||
width: $actualDimensions['width'],
|
||||
height: $actualDimensions['height'],
|
||||
filename: $filename,
|
||||
path: $image->path,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen Dateinamen für eine Bildvariante
|
||||
*/
|
||||
private function generateVariantFilename(string $originalFilename, string $type, string $size, string $format): string
|
||||
{
|
||||
$pathInfo = pathinfo($originalFilename);
|
||||
return $pathInfo['filename'] . "_{$type}_{$size}.{$format}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet das Bild mit ImageMagick
|
||||
*
|
||||
* @param string $sourcePath
|
||||
* @param string $destination
|
||||
* @param int $maxWidth
|
||||
* @param ImageFormat $format
|
||||
* @return array{width: int, height: int}
|
||||
*/
|
||||
private function processImage(string $sourcePath, string $destination, int $maxWidth, ImageFormat $format): array
|
||||
{
|
||||
try {
|
||||
$imagick = new Imagick($sourcePath);
|
||||
|
||||
// Bildorientierung korrigieren (EXIF)
|
||||
$imagick->autoOrientImage();
|
||||
|
||||
// Farbprofil entfernen für kleinere Dateien
|
||||
$imagick->stripImage();
|
||||
|
||||
// Bessere Resampling-Methode
|
||||
$imagick->setImageResolution(72, 72);
|
||||
|
||||
// Original-Dimensionen speichern
|
||||
$originalWidth = $imagick->getImageWidth();
|
||||
$originalHeight = $imagick->getImageHeight();
|
||||
|
||||
// Skalierung nur wenn nötig
|
||||
if ($originalWidth > $maxWidth) {
|
||||
$scale = $maxWidth / $originalWidth;
|
||||
$newWidth = $maxWidth;
|
||||
$newHeight = (int)round($originalHeight * $scale);
|
||||
|
||||
// Lanczos-Filter für beste Qualität
|
||||
$imagick->resizeImage($newWidth, $newHeight, Imagick::FILTER_LANCZOS, 1);
|
||||
|
||||
// Schärfen bei Verkleinerung
|
||||
$this->applySharpeningAfterResize($imagick, $originalWidth, $newWidth);
|
||||
} else {
|
||||
$newWidth = $originalWidth;
|
||||
$newHeight = $originalHeight;
|
||||
}
|
||||
|
||||
// Format-spezifische Optimierungen
|
||||
$this->optimizeForFormat($imagick, $format);
|
||||
|
||||
// Speichern
|
||||
$imagick->writeImage($destination);
|
||||
|
||||
$imagick->destroy();
|
||||
|
||||
return ['width' => $newWidth, 'height' => $newHeight];
|
||||
|
||||
} catch (ImagickException $e) {
|
||||
throw new \RuntimeException('ImageMagick error: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet formatspezifische Optimierungen an
|
||||
*/
|
||||
private function optimizeForFormat(Imagick $imagick, ImageFormat $format): void
|
||||
{
|
||||
match ($format) {
|
||||
ImageFormat::JPEG => $this->optimizeJpeg($imagick),
|
||||
ImageFormat::WEBP => $this->optimizeWebP($imagick),
|
||||
ImageFormat::AVIF => $this->optimizeAvif($imagick),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiert JPEG-Einstellungen
|
||||
*/
|
||||
private function optimizeJpeg(Imagick $imagick): void
|
||||
{
|
||||
$imagick->setImageFormat('jpeg');
|
||||
$imagick->setImageCompressionQuality(ImageFormat::JPEG->getQuality());
|
||||
$imagick->setImageCompression(Imagick::COMPRESSION_JPEG);
|
||||
|
||||
// Progressive JPEG für bessere Ladezeiten
|
||||
$imagick->setInterlaceScheme(Imagick::INTERLACE_PLANE);
|
||||
|
||||
// Chroma-Subsampling für kleinere Dateien
|
||||
$imagick->setSamplingFactors(['2x2', '1x1', '1x1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiert WebP-Einstellungen
|
||||
*/
|
||||
private function optimizeWebP(Imagick $imagick): void
|
||||
{
|
||||
$imagick->setImageFormat('webp');
|
||||
$imagick->setImageCompressionQuality(ImageFormat::WEBP->getQuality());
|
||||
|
||||
// WebP-spezifische Optionen
|
||||
$imagick->setOption('webp:lossless', 'false');
|
||||
$imagick->setOption('webp:alpha-quality', '85');
|
||||
$imagick->setOption('webp:method', '4'); // Balancierte Kompression
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiert AVIF-Einstellungen
|
||||
*/
|
||||
private function optimizeAvif(Imagick $imagick): void
|
||||
{
|
||||
// Wenn die ImageMagick-Version AVIF direkt unterstützt
|
||||
if (in_array('AVIF', Imagick::queryFormats())) {
|
||||
$imagick->setImageFormat('avif');
|
||||
$imagick->setImageCompressionQuality(ImageFormat::AVIF->getQuality());
|
||||
|
||||
// AVIF-spezifische Optionen, falls unterstützt
|
||||
$imagick->setOption('avif:speed', '6'); // Balancierte Kompression
|
||||
} else {
|
||||
// Fallback: Speichern als WebP wenn AVIF nicht unterstützt wird
|
||||
$this->optimizeWebP($imagick);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet intelligente Schärfung an, um Details zu verbessern
|
||||
*/
|
||||
private function applySharpeningAfterResize(Imagick $imagick, int $originalWidth, int $newWidth): void
|
||||
{
|
||||
$scaleFactor = $newWidth / $originalWidth;
|
||||
|
||||
// Nur bei signifikanter Verkleinerung schärfen
|
||||
if ($scaleFactor < 0.8) {
|
||||
// Stärke der Schärfung an die Verkleinerung anpassen
|
||||
$radius = 0.5;
|
||||
$sigma = 0.5;
|
||||
$amount = 0.8 + ((1 - $scaleFactor) * 0.3); // Mehr Schärfung bei größerer Verkleinerung
|
||||
$threshold = 0.05;
|
||||
|
||||
$imagick->unsharpMaskImage($radius, $sigma, $amount, $threshold);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user