setWidth($width); $naturalSize->setHeight($height); $drawing = new DrawingProperties; $drawing->setScaleBehavior($scaleBehavior); $drawing->setNaturalSize($naturalSize); $drawing->setAlphaType(AlphaType::ALPHA_TYPE_STRAIGHT); $fileProperties = new FileProperties; $fileProperties->setLocalUrl($this->bundleRelativeUrl($bundleRelativeFilename)); $image = new ImageTypeProperties; $image->setDrawing($drawing); $image->setFile($fileProperties); $metadata = new Metadata; $metadata->setFormat('JPG'); $media = new Media; $media->setUuid($this->newUuid()); $media->setUrl($this->bundleRelativeUrl($bundleRelativeFilename)); $media->setMetadata($metadata); $media->setImage($image); $fill = new Fill; $fill->setEnable(true); $fill->setMedia($media); $graphicsElement = new GraphicsElement; $graphicsElement->setUuid($this->newUuid()); $graphicsElement->setName($elementName); $graphicsElement->setBounds($this->fullFrameBounds($width, $height)); $graphicsElement->setOpacity(1.0); $graphicsElement->setPath($this->rectanglePath()); $graphicsElement->setFill($fill); $slideElement = new SlideElement; $slideElement->setElement($graphicsElement); $slideElement->setInfo(0); return $slideElement; } /** * Self-contained REPLACE: derive the per-cue plan from the Song's own * FOREGROUND image media actions, then strip those actions and inject content * elements. This is the method the export services call after * ProFileGenerator::generate(). BACKGROUND image actions are left in place. */ public function replaceImageActionsWithElements(Song $song): void { $this->injectIntoSong($song, $this->planFromSong($song)); } /** * Convert each cue's FOREGROUND (uploaded) image media action into a * full-frame content fill.media element appended on top of the generated * text/content elements, and strip that foreground action. The BACKGROUND * media action (service background / key-visual) is left untouched so it * keeps rendering as a background layer beneath all slide content. * * $plan is an ordered list (one entry per cue, in cue order). Each entry: * ['foreground' => ?string, 'foregroundName' => ?string] * A null/absent foreground leaves the cue exactly as generated (text-only * songs, key-visual-only slides, name-tag slides). * * @param array $plan */ public function injectIntoSong(Song $song, array $plan): void { $index = 0; foreach ($song->getPresentation()->getCues() as $cue) { $entry = $plan[$index] ?? []; $index++; $baseSlide = $this->baseSlideForCue($cue); if ($baseSlide === null) { continue; } $foreground = $entry['foreground'] ?? null; if ($foreground === null || $foreground === '') { // No foreground image: leave the cue (text elements, BACKGROUND // media action, macro) exactly as the generator produced it. continue; } $this->stripForegroundImageMediaActions($cue); $elements = []; foreach ($baseSlide->getElements() as $element) { $elements[] = $element; } $elements[] = $this->mediaFillElement($foreground, $entry['foregroundName'] ?? 'Bild'); $baseSlide->setElements($elements); } } /** * Derive the injection plan from the FOREGROUND image media actions the * generator added. BACKGROUND-layer image actions (service background / * key-visual) are intentionally ignored so they remain media actions. * * @return array */ private function planFromSong(Song $song): array { $plan = []; foreach ($song->getPresentation()->getCues() as $cue) { $foreground = null; $foregroundName = null; foreach ($cue->getActions() as $action) { if ($action->getType() !== ActionType::ACTION_TYPE_MEDIA) { continue; } $mediaType = $action->getMedia(); $mediaElement = $mediaType?->getElement(); if ($mediaElement === null || ! $mediaElement->hasImage()) { continue; } // Only FOREGROUND images become content elements. BACKGROUND-layer // images (service background / key-visual) stay as media actions. if ($mediaType->getLayerType() === LayerType::LAYER_TYPE_BACKGROUND) { continue; } $filename = $mediaElement->getUrl()?->getAbsoluteString(); if ($filename === null || $filename === '') { continue; } $foreground = $filename; $foregroundName = $action->getName() !== '' ? $action->getName() : null; } $plan[] = [ 'foreground' => $foreground, 'foregroundName' => $foregroundName, ]; } return $plan; } /** Resolve the base slide via Cue → SLIDE action → presentation → base slide. */ private function baseSlideForCue(Cue $cue): ?BaseSlide { foreach ($cue->getActions() as $action) { if ($action->getType() === ActionType::ACTION_TYPE_PRESENTATION_SLIDE) { return $action->getSlide()?->getPresentation()?->getBaseSlide(); } } return null; } /** * Remove only FOREGROUND-layer media actions whose element carries an image. * BACKGROUND-layer image actions (service background / key-visual) are kept. */ private function stripForegroundImageMediaActions(Cue $cue): void { $kept = []; $stripped = false; foreach ($cue->getActions() as $action) { if ($action->getType() === ActionType::ACTION_TYPE_MEDIA) { $mediaType = $action->getMedia(); $mediaElement = $mediaType?->getElement(); if ( $mediaElement !== null && $mediaElement->hasImage() && $mediaType->getLayerType() !== LayerType::LAYER_TYPE_BACKGROUND ) { $stripped = true; continue; } } $kept[] = $action; } if ($stripped) { $cue->setActions($kept); } } private function bundleRelativeUrl(string $filename): URL { $local = new LocalRelativePath; $local->setRoot(LocalRelativePath\Root::ROOT_CURRENT_RESOURCE); $local->setPath($filename); $url = new URL; $url->setAbsoluteString($filename); $url->setLocal($local); $url->setPlatform(UrlPlatform::PLATFORM_MACOS); return $url; } private function fullFrameBounds(int $width, int $height): Rect { $origin = new Point; $origin->setX(0); $origin->setY(0); $size = new Size; $size->setWidth($width); $size->setHeight($height); $rect = new Rect; $rect->setOrigin($origin); $rect->setSize($size); return $rect; } private function rectanglePath(): Path { $points = []; foreach ([[0, 0], [1, 0], [1, 1], [0, 1]] as [$x, $y]) { $point = new Point; $point->setX((float) $x); $point->setY((float) $y); $bezier = new BezierPoint; $bezier->setPoint($point); $bezier->setQ0($point); $bezier->setQ1($point); $bezier->setCurved(false); $points[] = $bezier; } $path = new Path; $path->setClosed(true); $path->setPoints($points); $shape = new Shape; $shape->setType(ShapeType::TYPE_RECTANGLE); $path->setShape($shape); return $path; } private function newUuid(): UUID { $bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 0x0F) | 0x40); $bytes[8] = chr((ord($bytes[8]) & 0x3F) | 0x80); $hex = bin2hex($bytes); $uuid = new UUID; $uuid->setString(strtoupper(sprintf( '%s-%s-%s-%s-%s', substr($hex, 0, 8), substr($hex, 8, 4), substr($hex, 12, 4), substr($hex, 16, 4), substr($hex, 20, 12), ))); return $uuid; } }