pp-planer/app/Services/FileConversionService.php
2026-05-30 23:56:05 +02:00

354 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use App\Jobs\ConvertPowerPointJob;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use ZipArchive;
class FileConversionService
{
private const MAX_FILE_SIZE_BYTES = 52428800;
private const SUPPORTED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'ppt', 'pptx', 'zip'];
private const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'];
private const POWERPOINT_EXTENSIONS = ['ppt', 'pptx'];
public function convertImage(UploadedFile|string|SplFileInfo $file): array
{
$sourcePath = $this->resolvePath($file);
$extension = $this->resolveExtension($file, $sourcePath);
$this->assertSupported($extension);
if (! in_array($extension, self::IMAGE_EXTENSIONS, true)) {
throw new InvalidArgumentException('Nur Bilddateien koennen mit convertImage verarbeitet werden.');
}
$this->assertSize($file, $sourcePath);
$filename = Str::uuid()->toString().'.jpg';
$relativePath = 'slides/'.$filename;
$targetPath = Storage::disk('public')->path($relativePath);
Storage::disk('public')->makeDirectory('slides');
$this->ensureDirectory(dirname($targetPath));
$manager = $this->createImageManager();
$image = $manager->read($sourcePath);
$originalWidth = $image->width();
$originalHeight = $image->height();
$warnings = $this->checkImageDimensions($originalWidth, $originalHeight);
$image->contain(1920, 1080, '000000', 'center');
$image->save($targetPath, quality: 90);
$thumbnailPath = $this->generateThumbnail($relativePath);
return [
'filename' => $relativePath,
'thumbnail' => $thumbnailPath,
'warnings' => $warnings,
'fullCover' => false,
];
}
public function convertImageCover(UploadedFile|string|SplFileInfo $file, string $disk = 'public'): array
{
$sourcePath = $this->resolvePath($file);
$extension = $this->resolveExtension($file, $sourcePath);
$this->assertSupported($extension);
if (! in_array($extension, self::IMAGE_EXTENSIONS, true)) {
throw new InvalidArgumentException('Nur Bilddateien koennen mit convertImageCover verarbeitet werden.');
}
$this->assertSize($file, $sourcePath);
$filename = Str::uuid()->toString().'.jpg';
$relativePath = 'slides/'.$filename;
$targetPath = Storage::disk($disk)->path($relativePath);
Storage::disk($disk)->makeDirectory('slides');
$this->ensureDirectory(dirname($targetPath));
$manager = $this->createImageManager();
$image = $manager->read($sourcePath);
$originalWidth = $image->width();
$originalHeight = $image->height();
$warnings = $this->checkCoverImageDimensions($originalWidth, $originalHeight);
$image->cover(1920, 1080);
$image->save($targetPath, quality: 90);
$thumbnailPath = $this->generateThumbnail($relativePath, $disk);
return [
'filename' => $relativePath,
'thumbnail' => $thumbnailPath,
'warnings' => $warnings,
'fullCover' => true,
];
}
public function convertPowerPoint(UploadedFile|string|SplFileInfo $file): string
{
$sourcePath = $this->resolvePath($file);
$extension = $this->resolveExtension($file, $sourcePath);
$this->assertSupported($extension);
if (! in_array($extension, self::POWERPOINT_EXTENSIONS, true)) {
throw new InvalidArgumentException('Nur PPT/PPTX-Dateien sind hier erlaubt.');
}
$this->assertSize($file, $sourcePath);
$jobId = Str::uuid()->toString();
ConvertPowerPointJob::dispatch($sourcePath, $jobId);
return $jobId;
}
public function processZip(UploadedFile|string|SplFileInfo $file): array
{
$sourcePath = $this->resolvePath($file);
$extension = $this->resolveExtension($file, $sourcePath);
$this->assertSupported($extension);
if ($extension !== 'zip') {
throw new InvalidArgumentException('processZip erwartet eine ZIP-Datei.');
}
$this->assertSize($file, $sourcePath);
$zip = new ZipArchive;
if ($zip->open($sourcePath) !== true) {
throw new InvalidArgumentException('ZIP-Datei konnte nicht geoeffnet werden.');
}
$extractDir = storage_path('app/temp/zip-'.Str::uuid()->toString());
if (! is_dir($extractDir) && ! mkdir($extractDir, 0775, true) && ! is_dir($extractDir)) {
throw new InvalidArgumentException('Temporaires ZIP-Verzeichnis konnte nicht erstellt werden.');
}
$zip->extractTo($extractDir);
$zip->close();
$results = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($extractDir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $entry) {
if (! $entry instanceof SplFileInfo || ! $entry->isFile()) {
continue;
}
$entryPath = $entry->getRealPath();
if ($entryPath === false) {
continue;
}
$entryExtension = $this->extensionFromPath($entryPath);
if (! in_array($entryExtension, self::SUPPORTED_EXTENSIONS, true)) {
continue;
}
if (in_array($entryExtension, self::IMAGE_EXTENSIONS, true)) {
$results[] = $this->convertImage($entryPath);
continue;
}
if (in_array($entryExtension, self::POWERPOINT_EXTENSIONS, true)) {
$results[] = ['job_id' => $this->convertPowerPoint($entryPath)];
continue;
}
$results = [...$results, ...$this->processZip($entryPath)];
}
$this->deleteDirectory($extractDir);
return $results;
}
public function generateThumbnail(string $path, string $disk = 'public'): string
{
$absolutePath = str_starts_with($path, DIRECTORY_SEPARATOR)
? $path
: Storage::disk($disk)->path($path);
if (! is_file($absolutePath)) {
throw new InvalidArgumentException('Datei fuer Thumbnail nicht gefunden.');
}
$filename = pathinfo($absolutePath, PATHINFO_FILENAME).'.jpg';
$thumbnailRelativePath = 'slides/thumbnails/'.$filename;
$thumbnailAbsolutePath = Storage::disk($disk)->path($thumbnailRelativePath);
Storage::disk($disk)->makeDirectory('slides/thumbnails');
$this->ensureDirectory(dirname($thumbnailAbsolutePath));
$manager = $this->createImageManager();
$canvas = $manager->create(320, 180)->fill('000000');
$image = $manager->read($absolutePath);
$image->scaleDown(width: 320, height: 180);
$canvas->place($image, 'center');
$canvas->save($thumbnailAbsolutePath, quality: 85);
return $thumbnailRelativePath;
}
private function resolvePath(UploadedFile|string|SplFileInfo $file): string
{
if ($file instanceof UploadedFile) {
$path = $file->getRealPath();
if ($path === false || ! is_file($path)) {
throw new InvalidArgumentException('Upload-Datei ist ungueltig.');
}
return $path;
}
if ($file instanceof SplFileInfo) {
$path = $file->getRealPath();
if ($path === false) {
throw new InvalidArgumentException('Dateipfad ist ungueltig.');
}
return $path;
}
if (! is_file($file)) {
throw new InvalidArgumentException('Datei wurde nicht gefunden.');
}
return $file;
}
private function assertSupported(string $extension): void
{
if (! in_array($extension, self::SUPPORTED_EXTENSIONS, true)) {
throw new InvalidArgumentException('Dateityp wird nicht unterstuetzt.');
}
}
private function extensionFromPath(string $path): string
{
return strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
}
private function resolveExtension(UploadedFile|string|SplFileInfo $file, string $sourcePath): string
{
if ($file instanceof UploadedFile) {
return strtolower($file->getClientOriginalExtension());
}
return $this->extensionFromPath($sourcePath);
}
private function assertSize(UploadedFile|string|SplFileInfo $file, string $resolvedPath): void
{
$size = $file instanceof UploadedFile
? ($file->getSize() ?? 0)
: (filesize($resolvedPath) ?: 0);
if ($size > self::MAX_FILE_SIZE_BYTES) {
throw new InvalidArgumentException('Datei ist groesser als 50MB.');
}
}
private function deleteDirectory(string $directory): void
{
if (! is_dir($directory)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item) {
if (! $item instanceof SplFileInfo) {
continue;
}
if ($item->isDir()) {
@rmdir($item->getPathname());
continue;
}
@unlink($item->getPathname());
}
@rmdir($directory);
}
/** @return string[] */
private function checkImageDimensions(int $width, int $height): array
{
$warnings = [];
$isExactOrLarger16by9 = $width >= 1920 && $height >= 1080
&& abs($width / $height - 16 / 9) < 0.01;
if (! $isExactOrLarger16by9) {
$warnings[] = 'Das Bild hat nicht das optimale Seitenverhältnis von 16:9. '
.'Für die beste Darstellung verwende bitte Bilder mit exakt 1920×1080 Pixeln. '
.'Das Bild wurde trotzdem verarbeitet, es können aber schwarze Ränder entstehen.';
}
if ($width < 1920 || $height < 1080) {
$warnings[] = "Das Bild ({$width}×{$height}) ist kleiner als 1920×1080 und wurde hochskaliert. "
.'Dadurch kann die Qualität schlechter sein. '
.'Lade am besten Bilder mit mindestens 1920×1080 Pixeln hoch.';
}
return $warnings;
}
/** @return string[] */
private function checkCoverImageDimensions(int $width, int $height): array
{
$warnings = [];
if ($width < 1920 || $height < 1080) {
$warnings[] = "Das Bild ({$width}×{$height}) ist kleiner als 1920×1080 und wurde hochskaliert. "
.'Dadurch kann die Qualität schlechter sein. '
.'Lade am besten Bilder mit mindestens 1920×1080 Pixeln hoch.';
}
return $warnings;
}
private function ensureDirectory(string $directory): void
{
if (is_dir($directory)) {
return;
}
if (! mkdir($directory, 0775, true) && ! is_dir($directory)) {
throw new InvalidArgumentException('Zielverzeichnis konnte nicht erstellt werden.');
}
}
private function createImageManager(): mixed
{
$managerClass = implode('\\', ['Intervention', 'Image', 'ImageManager']);
$driverClass = implode('\\', ['Intervention', 'Image', 'Drivers', 'Gd', 'Driver']);
return new $managerClass(new $driverClass);
}
}