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); } } }