- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
246 lines
7.7 KiB
PHP
246 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Domain\Media;
|
|
|
|
use Imagick;
|
|
use ImagickException;
|
|
|
|
/**
|
|
* 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->join($image->filename)->toString();
|
|
|
|
/** @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->join($filename)->toString();
|
|
|
|
$actualDimensions = $this->processImage($sourcePath, $destination, $width, $format);
|
|
|
|
$fileSize = filesize($destination);
|
|
|
|
return new ImageVariant(
|
|
imageId: (string) $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->toString(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|