slides() ->where('type', $blockType) ->orderBy('sort_order') ->get(); $groupName = ucfirst($blockType); return $this->buildBundleFromSlides($slides, $groupName, $service, $blockType, $blockType); } public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string { $agendaItem->loadMissing([ 'service', 'slides', 'serviceSong.song.arrangements.arrangementSections.section.slides', 'serviceSong.song.arrangements.arrangementSections.section.label', ]); $title = $agendaItem->title ?: 'Ablauf-Element'; if ($agendaItem->serviceSong?->song_id && $agendaItem->serviceSong->song) { $song = $agendaItem->serviceSong->song; $labelCount = $song->arrangements() ->withCount('arrangementLabels') ->get() ->sum('arrangement_labels_count'); if ($labelCount === 0) { throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.'); } $parserSong = app(ProExportService::class)->generateParserSong($song, $agendaItem->service); $proFilename = self::safeFilename($song->title).'.pro'; $songMediaFiles = []; $this->embedBackground($agendaItem->service, $songMediaFiles); $bundle = new PresentationBundle($parserSong, $proFilename, $songMediaFiles); $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, $agendaItem->service, 'agenda_item', $this->backgroundPartTypeForAgendaItem($agendaItem), ); } /** @param \Illuminate\Database\Eloquent\Collection $slides */ private function buildBundleFromSlides( $slides, string $groupName, ?Service $service = null, ?string $partType = null, ?string $backgroundPartType = null, ): string { $slideData = []; $mediaFiles = []; $background = $this->backgroundData($service); $backgroundAttached = false; 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; $singleSlideData = [ 'media' => $imageFilename, 'format' => 'JPG', 'label' => $slide->original_filename, ]; if ($this->shouldAttachBackground($slide, $backgroundPartType, $background)) { $singleSlideData['background'] = $background; $backgroundAttached = true; } if ($service !== null && $partType !== null) { $slideIndex = count($slideData); $totalSlides = $slides->count(); $macros = $this->macroResolutionService->macrosForSlide( $service, $partType, ['index' => $slideIndex, 'total' => $totalSlides, 'label_id' => null], ); if (! empty($macros)) { // ProPresenter parser currently supports one `macro` entry per slide $singleSlideData['macro'] = $macros[0]; } } $slideData[] = $singleSlideData; } if ($backgroundAttached) { $this->embedBackground($service, $mediaFiles); } $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 function backgroundData(?Service $service): ?array { if ($this->backgroundSourcePath($service) === null) { return null; } return [ 'path' => ServiceImageResolver::BACKGROUND_EXPORT_NAME, 'format' => 'JPG', 'width' => 1920, 'height' => 1080, 'bundleRelative' => true, ]; } /** Absolute filesystem path of the resolved background image, or null. */ private function backgroundSourcePath(?Service $service): ?string { if ($service === null) { return null; } $background = $this->imageResolver->backgroundFor($service); if ($background === null || ! Storage::disk('public')->exists($background)) { return null; } return Storage::disk('public')->path($background); } /** Embed the resolved background image bytes into the bundle under the fixed export name. */ private function embedBackground(?Service $service, array &$mediaFiles): void { $sourcePath = $this->backgroundSourcePath($service); if ($sourcePath === null || isset($mediaFiles[ServiceImageResolver::BACKGROUND_EXPORT_NAME])) { return; } $contents = @file_get_contents($sourcePath); if ($contents !== false) { $mediaFiles[ServiceImageResolver::BACKGROUND_EXPORT_NAME] = $contents; } } private function shouldAttachBackground(Slide $slide, ?string $backgroundPartType, ?array $background): bool { if ($background === null) { return false; } if ($backgroundPartType !== 'sermon' && $slide->type !== 'sermon') { return false; } return $slide->cover_mode !== true; } private function backgroundPartTypeForAgendaItem(ServiceAgendaItem $agendaItem): string { if ($agendaItem->slides->contains(fn (Slide $slide) => $slide->type === 'sermon')) { return 'sermon'; } $sermonPatterns = Setting::get('agenda_sermon_matching'); if ($sermonPatterns === null) { return 'agenda_item'; } $patterns = array_map('trim', explode(',', $sermonPatterns)); return app(AgendaMatcherService::class)->matchesAny($agendaItem->title, $patterns) ? 'sermon' : 'agenda_item'; } private static function safeFilename(string $name): string { return preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $name) ?: 'presentation'; } }