pp-planer/app/Services/ProBundleExportService.php
Thorsten Bus 0e3c647cfc feat: probundle export with media, image upscaling, upload dimension warnings
- Fix probundle exports missing images (double slides/ prefix in storage path)
- Replace manual ZipArchive with PresentationBundle + ProBundleWriter from parser plugin
- Add per-agenda-item download route and buttons for songs and slide items
- Remove text layer from image-only slides in .pro generation
- Fix image conversion: upscale small images, black bars on 2 sides max (contain)
- Add upload warnings for non-16:9 and sub-1920x1080 images (German, non-blocking)
- Update SlideFactory and all tests to use slides/ prefix in stored_filename
- Add 11 new tests (agenda download, image conversion, upload warnings)
2026-03-30 10:29:37 +02:00

126 lines
3.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\Service;
use App\Models\ServiceAgendaItem;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use ProPresenter\Parser\PresentationBundle;
use ProPresenter\Parser\ProBundleWriter;
use ProPresenter\Parser\ProFileGenerator;
use RuntimeException;
class ProBundleExportService
{
private const ALLOWED_BLOCK_TYPES = ['information', 'moderation', 'sermon'];
public function generateBundle(Service $service, string $blockType): string
{
if (! in_array($blockType, self::ALLOWED_BLOCK_TYPES, true)) {
throw new InvalidArgumentException('Ungültiger Blocktyp für .probundle Export.');
}
$slides = $service->slides()
->where('type', $blockType)
->orderBy('sort_order')
->get();
$groupName = ucfirst($blockType);
return $this->buildBundleFromSlides($slides, $groupName);
}
public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string
{
$agendaItem->loadMissing([
'slides',
'serviceSong.song.groups.slides',
'serviceSong.song.arrangements.arrangementGroups.group',
]);
$title = $agendaItem->title ?: 'Ablauf-Element';
if ($agendaItem->serviceSong?->song_id && $agendaItem->serviceSong->song) {
$song = $agendaItem->serviceSong->song;
if ($song->groups()->count() === 0) {
throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.');
}
$parserSong = (new ProExportService)->generateParserSong($song);
$proFilename = self::safeFilename($song->title).'.pro';
$bundle = new PresentationBundle($parserSong, $proFilename);
$bundlePath = sys_get_temp_dir().'/'.uniqid('agenda-song-').'.probundle';
ProBundleWriter::write($bundle, $bundlePath);
return $bundlePath;
}
$slides = $agendaItem->slides()
->whereNull('deleted_at')
->orderBy('sort_order')
->get();
return $this->buildBundleFromSlides($slides, $title);
}
/** @param \Illuminate\Database\Eloquent\Collection<int, \App\Models\Slide> $slides */
private function buildBundleFromSlides($slides, string $groupName): string
{
$slideData = [];
$mediaFiles = [];
foreach ($slides as $slide) {
$sourcePath = Storage::disk('public')->path($slide->stored_filename);
if (! file_exists($sourcePath)) {
continue;
}
$imageFilename = basename($slide->stored_filename);
$imageContent = file_get_contents($sourcePath);
if ($imageContent === false) {
continue;
}
$mediaFiles[$imageFilename] = $imageContent;
$slideData[] = [
'media' => $imageFilename,
'format' => 'JPG',
'label' => $slide->original_filename,
];
}
$groups = [
[
'name' => $groupName,
'color' => [0, 0, 0, 1],
'slides' => $slideData,
],
];
$arrangements = [
[
'name' => 'normal',
'groupNames' => [$groupName],
],
];
$song = ProFileGenerator::generate($groupName, $groups, $arrangements);
$proFilename = self::safeFilename($groupName).'.pro';
$bundle = new PresentationBundle($song, $proFilename, $mediaFiles);
$bundlePath = sys_get_temp_dir().'/'.uniqid('bundle-').'.probundle';
ProBundleWriter::write($bundle, $bundlePath);
return $bundlePath;
}
private static function safeFilename(string $name): string
{
return preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $name) ?: 'presentation';
}
}