feat(images): cover-fit conversion mode
This commit is contained in:
parent
0a345aa3b2
commit
73a523d0e1
7
.sisyphus/evidence/task-2-contain-regression.txt
Normal file
7
.sisyphus/evidence/task-2-contain-regression.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
PASS Tests\Feature\FileConversionServiceTest
|
||||||
|
✓ contain conversion keeps black bars and fullCover false 0.89s
|
||||||
|
|
||||||
|
Tests: 1 passed (12 assertions)
|
||||||
|
Duration: 1.06s
|
||||||
|
|
||||||
16
.sisyphus/evidence/task-2-cover-fill.txt
Normal file
16
.sisyphus/evidence/task-2-cover-fill.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
PASS Tests\Feature\FileConversionServiceTest
|
||||||
|
✓ cover conversion fills 1920x1080 without black bars 0.71s
|
||||||
|
✓ contain conversion keeps black bars and fullCover false 0.16s
|
||||||
|
✓ cover conversion upscales small sources with German quality warning 0.16s
|
||||||
|
|
||||||
|
PASS Tests\Feature\FileConversionTest
|
||||||
|
✓ convert image creates 1920x1080 jpg with black bars and thumbnail 0.13s
|
||||||
|
✓ small image is upscaled with at most two black bars 0.14s
|
||||||
|
✓ exact 16:9 image has no black bars 0.26s
|
||||||
|
✓ small 16:9 image is upscaled without black bars 0.17s
|
||||||
|
✓ portrait image gets pillarbox bars on left and right 0.21s
|
||||||
|
|
||||||
|
Tests: 8 passed (67 assertions)
|
||||||
|
Duration: 2.11s
|
||||||
|
|
||||||
|
|
@ -56,6 +56,45 @@ public function convertImage(UploadedFile|string|SplFileInfo $file): array
|
||||||
'filename' => $relativePath,
|
'filename' => $relativePath,
|
||||||
'thumbnail' => $thumbnailPath,
|
'thumbnail' => $thumbnailPath,
|
||||||
'warnings' => $warnings,
|
'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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,11 +182,11 @@ public function processZip(UploadedFile|string|SplFileInfo $file): array
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateThumbnail(string $path): string
|
public function generateThumbnail(string $path, string $disk = 'public'): string
|
||||||
{
|
{
|
||||||
$absolutePath = str_starts_with($path, DIRECTORY_SEPARATOR)
|
$absolutePath = str_starts_with($path, DIRECTORY_SEPARATOR)
|
||||||
? $path
|
? $path
|
||||||
: Storage::disk('public')->path($path);
|
: Storage::disk($disk)->path($path);
|
||||||
|
|
||||||
if (! is_file($absolutePath)) {
|
if (! is_file($absolutePath)) {
|
||||||
throw new InvalidArgumentException('Datei fuer Thumbnail nicht gefunden.');
|
throw new InvalidArgumentException('Datei fuer Thumbnail nicht gefunden.');
|
||||||
|
|
@ -155,8 +194,8 @@ public function generateThumbnail(string $path): string
|
||||||
|
|
||||||
$filename = pathinfo($absolutePath, PATHINFO_FILENAME).'.jpg';
|
$filename = pathinfo($absolutePath, PATHINFO_FILENAME).'.jpg';
|
||||||
$thumbnailRelativePath = 'slides/thumbnails/'.$filename;
|
$thumbnailRelativePath = 'slides/thumbnails/'.$filename;
|
||||||
$thumbnailAbsolutePath = Storage::disk('public')->path($thumbnailRelativePath);
|
$thumbnailAbsolutePath = Storage::disk($disk)->path($thumbnailRelativePath);
|
||||||
Storage::disk('public')->makeDirectory('slides/thumbnails');
|
Storage::disk($disk)->makeDirectory('slides/thumbnails');
|
||||||
$this->ensureDirectory(dirname($thumbnailAbsolutePath));
|
$this->ensureDirectory(dirname($thumbnailAbsolutePath));
|
||||||
|
|
||||||
$manager = $this->createImageManager();
|
$manager = $this->createImageManager();
|
||||||
|
|
@ -279,6 +318,20 @@ private function checkImageDimensions(int $width, int $height): array
|
||||||
return $warnings;
|
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
|
private function ensureDirectory(string $directory): void
|
||||||
{
|
{
|
||||||
if (is_dir($directory)) {
|
if (is_dir($directory)) {
|
||||||
|
|
|
||||||
94
tests/Feature/FileConversionServiceTest.php
Normal file
94
tests/Feature/FileConversionServiceTest.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\FileConversionService;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Storage::fake('public');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cover conversion fills 1920x1080 without black bars', function () {
|
||||||
|
$service = app(FileConversionService::class);
|
||||||
|
$file = makePngUploadForFileConversionService('cover.png', 1200, 900);
|
||||||
|
|
||||||
|
$result = $service->convertImageCover($file);
|
||||||
|
|
||||||
|
expect($result)->toHaveKeys(['filename', 'thumbnail', 'warnings', 'fullCover']);
|
||||||
|
expect($result['fullCover'])->toBeTrue();
|
||||||
|
expect(Storage::disk('public')->exists($result['filename']))->toBeTrue();
|
||||||
|
expect(Storage::disk('public')->exists($result['thumbnail']))->toBeTrue();
|
||||||
|
|
||||||
|
$mainPath = Storage::disk('public')->path($result['filename']);
|
||||||
|
[$width, $height] = getimagesize($mainPath);
|
||||||
|
|
||||||
|
expect($width)->toBe(1920);
|
||||||
|
expect($height)->toBe(1080);
|
||||||
|
|
||||||
|
$image = imagecreatefromjpeg($mainPath);
|
||||||
|
expect($image)->not->toBeFalse();
|
||||||
|
|
||||||
|
foreach ([[0, 0], [1919, 0], [0, 1079], [1919, 1079]] as [$x, $y]) {
|
||||||
|
$corner = imagecolorsforindex($image, imagecolorat($image, $x, $y));
|
||||||
|
expect($corner['red'] + $corner['green'] + $corner['blue'])->toBeGreaterThan(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('contain conversion keeps black bars and fullCover false', function () {
|
||||||
|
$service = app(FileConversionService::class);
|
||||||
|
$file = makePngUploadForFileConversionService('contain.png', 1200, 900);
|
||||||
|
|
||||||
|
$result = $service->convertImage($file);
|
||||||
|
|
||||||
|
expect($result)->toHaveKeys(['filename', 'thumbnail', 'warnings', 'fullCover']);
|
||||||
|
expect($result['fullCover'])->toBeFalse();
|
||||||
|
|
||||||
|
$mainPath = Storage::disk('public')->path($result['filename']);
|
||||||
|
[$width, $height] = getimagesize($mainPath);
|
||||||
|
|
||||||
|
expect($width)->toBe(1920);
|
||||||
|
expect($height)->toBe(1080);
|
||||||
|
|
||||||
|
$image = imagecreatefromjpeg($mainPath);
|
||||||
|
expect($image)->not->toBeFalse();
|
||||||
|
|
||||||
|
foreach ([[0, 0], [1919, 0], [0, 1079], [1919, 1079]] as [$x, $y]) {
|
||||||
|
$corner = imagecolorsforindex($image, imagecolorat($image, $x, $y));
|
||||||
|
expect($corner['red'] + $corner['green'] + $corner['blue'])->toBeLessThan(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cover conversion upscales small sources with German quality warning', function () {
|
||||||
|
$service = app(FileConversionService::class);
|
||||||
|
$file = makePngUploadForFileConversionService('small-cover.png', 800, 600);
|
||||||
|
|
||||||
|
$result = $service->convertImageCover($file);
|
||||||
|
|
||||||
|
$mainPath = Storage::disk('public')->path($result['filename']);
|
||||||
|
[$width, $height] = getimagesize($mainPath);
|
||||||
|
|
||||||
|
expect($width)->toBe(1920);
|
||||||
|
expect($height)->toBe(1080);
|
||||||
|
expect($result['warnings'])->not->toBeEmpty();
|
||||||
|
expect(implode(' ', $result['warnings']))->toContain('kleiner als 1920×1080');
|
||||||
|
expect(implode(' ', $result['warnings']))->toContain('hochskaliert');
|
||||||
|
});
|
||||||
|
|
||||||
|
function makePngUploadForFileConversionService(string $name, int $width, int $height): UploadedFile
|
||||||
|
{
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'cts-cover-');
|
||||||
|
if ($path === false) {
|
||||||
|
throw new RuntimeException('Temp-Datei konnte nicht erstellt werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$image = imagecreatetruecolor($width, $height);
|
||||||
|
if ($image === false) {
|
||||||
|
throw new RuntimeException('Bild konnte nicht erstellt werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$red = imagecolorallocate($image, 255, 0, 0);
|
||||||
|
imagefill($image, 0, 0, $red);
|
||||||
|
imagepng($image, $path);
|
||||||
|
|
||||||
|
return new UploadedFile($path, $name, 'image/png', null, true);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue