266 lines
8.6 KiB
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);
|
|
}
|
|
|
|
}
|