['width' => 150, 'height' => 150, 'crop' => true], 'small' => ['width' => 400, 'height' => 400, 'crop' => false], 'medium' => ['width' => 800, 'height' => 800, 'crop' => false], 'large' => ['width' => 1200, 'height' => 1200, 'crop' => false], ]; private const array FORMATS = ['jpg', 'webp', 'avif']; public function __construct( private PathProvider $pathProvider, private ImageRepository $imageRepository, private ImageVariantRepository $variantRepository, ) {} public function uploadImage(UploadedFile $file): Image { // Validierung if (!$file->isValid()) { throw new \InvalidArgumentException('Ungültige Datei'); } if (!$this->isValidImageType($file->getMimeType())) { throw new \InvalidArgumentException('Ungültiger Bildtyp'); } // Basis-Image-Informationen erfassen $imageInfo = getimagesize($file->tmpName); $hash = \hash_file('sha256', $file->tmpName); // Prüfen ob ein Bild mit gleichem Hash bereits existiert $existingImage = $this->imageRepository->findByHash($hash); if ($existingImage) { return $existingImage; // Duplikat gefunden, vorhandenes Bild zurückgeben } // Image Entity erstellen $image = new Image( filename: $this->generateSecureFilename($file->name), originalFilename: $file->name, mimeType: $file->getMimeType(), fileSize: $file->size, width: $imageInfo[0], height: $imageInfo[1], hash: $hash, uploadPath: '', ); // In Datenbank speichern um ID zu erhalten $image = $this->imageRepository->save($image); // Upload-Pfad setzen $image->uploadPath = $image->getUploadDirectory() . '/' . $image->getFilePathPattern(); $this->imageRepository->update($image); // Ordnerstruktur erstellen $fullUploadPath = $this->pathProvider->resolvePath('storage' . $image->uploadPath); $this->createDirectoryStructure($fullUploadPath); // Original-Datei speichern $originalPath = $fullUploadPath . '/original.' . $this->getFileExtension($file->getMimeType()); $file->moveTo($originalPath); // Varianten erstellen $this->createImageVariants($image, $originalPath); // Image in Datenbank aktualisieren #$this->imageRepository->update($image); return $image; } private function createImageVariants(Image $image, string $originalPath): void { foreach (self::VARIANTS as $variantName => $config) { foreach (self::FORMATS as $format) { $variant = $this->createImageVariant( $image, $originalPath, $variantName, $format, $config ); // In Datenbank speichern $this->variantRepository->save($variant); } } } private function createImageVariant( Image $image, string $originalPath, string $variantName, string $format, array $config ): ImageVariant { $outputPath = $this->pathProvider->resolvePath('storage' . $image->uploadPath . '/' . $variantName . '.' . $format); // Bild verarbeiten $resizedImage = $this->resizeImage($originalPath, $config['width'], $config['height'], $config['crop'] ?? false); $this->saveImageInFormat($resizedImage, $outputPath, $format); // Neue Dateigröße und Dimensionen ermitteln $newImageInfo = getimagesize($outputPath); $fileSize = filesize($outputPath); return new ImageVariant( imageId: $image->id, variant: $variantName, format: $format, width: $newImageInfo[0], height: $newImageInfo[1], fileSize: $fileSize, filename: $variantName . '.' . $format ); } private function resizeImage(string $sourcePath, int $maxWidth, int $maxHeight, bool $crop = false): \GdImage { $sourceImage = imagecreatefromstring(\file_get_contents($sourcePath)); $sourceWidth = imagesx($sourceImage); $sourceHeight = imagesy($sourceImage); if ($crop) { // Crop-Logik für quadratische Thumbnails $ratio = max($maxWidth / $sourceWidth, $maxHeight / $sourceHeight); $newWidth = (int)round($sourceWidth * $ratio); $newHeight = (int)round($sourceHeight * $ratio); $tempImage = imagecreatetruecolor($newWidth, $newHeight); imagecopyresampled($tempImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight); $finalImage = imagecreatetruecolor($maxWidth, $maxHeight); $cropX = (int)floor(($newWidth - $maxWidth) / 2); $cropY = (int)floor(($newHeight - $maxHeight) / 2); \imagecopy($finalImage, $tempImage, 0, 0, $cropX, $cropY, $maxWidth, $maxHeight); imagedestroy($tempImage); } else { // Proportionales Skalieren $ratio = min($maxWidth / $sourceWidth, $maxHeight / $sourceHeight); $newWidth = (int)round($sourceWidth * $ratio); $newHeight = (int)round($sourceHeight * $ratio); $finalImage = imagecreatetruecolor($newWidth, $newHeight); imagecopyresampled($finalImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight); } imagedestroy($sourceImage); return $finalImage; } private function saveImageInFormat(\GdImage $image, string $path, string $format): void { switch ($format) { case 'jpg': imagejpeg($image, $path, 85); break; case 'webp': imagewebp($image, $path, 85); break; case 'avif': if (\function_exists('imageavif')) { imageavif($image, $path, 85); } else { // Fallback auf WebP wenn AVIF nicht verfügbar imagewebp($image, $path, 85); } break; } imagedestroy($image); } private function createDirectoryStructure(string $path): void { if (!is_dir($path)) { mkdir($path, 0755, true); } } private function isValidImageType(string $mimeType): bool { return in_array($mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ]); } private function getFileExtension(string $mimeType): string { return match ($mimeType) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', default => 'jpg' }; } private function generateSecureFilename(string $originalName): string { $extension = pathinfo($originalName, PATHINFO_EXTENSION); return uniqid('img_', true) . '.' . strtolower($extension); } public function getImageUrl(Image $image, string $variant = 'medium', string $format = 'jpg'): string { return '/media/' . $image->getUrlId() . '-' . $variant . '.' . $format; } public function findById(int $id): ?Image { return $this->imageRepository->findById($id); } public function getVariants(Image $image): array { return $this->variantRepository->findByImageId($image->id); } /** * Löst eine URL-ID zu einem Image-Objekt auf */ public function resolveFromUrlId(string $urlId): ?Image { // URL-ID format: YYYYMMDDXXXXXXXX (Datum + 8-stellige ID) if (strlen($urlId) !== 16) { return null; } $idPart = substr($urlId, 8); // Die letzten 8 Zeichen $id = (int)ltrim($idPart, '0'); // Führende Nullen entfernen return $this->imageRepository->findById($id); } }