Files
michaelschiemer/.archive/Media/Services/ImageService.php

266 lines
8.6 KiB
PHP

<?php
namespace Media\Services;
use App\Framework\Core\PathProvider;
use App\Framework\Http\UploadedFile;
use Media\Entities\Image;
use Media\Entities\ImageVariant;
use Media\Repositories\ImageRepository;
use Media\Repositories\ImageVariantRepository;
use function filesize;
use function getimagesize;
use function imageavif;
use function imagecopyresampled;
use function imagecreatefromstring;
use function imagecreatetruecolor;
use function imagedestroy;
use function imagejpeg;
use function imagesx;
use function imagesy;
use function imagewebp;
class ImageService
{
private const array VARIANTS = [
'thumbnail' => ['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);
}
}