pp-planer/tests/Support/InspectsSlideFillMedia.php
Thorsten Bus 94170857b6 feat(export): render uploaded slide images as slide content, keep background/keyvisual as background media action
- New SlideMediaElementBuilder converts FOREGROUND (uploaded) image media
  actions into slide-content fill.media elements placed at the top of the
  slide content, so uploaded information/moderation/sermon images render as
  real content rather than a media-layer action.
- Service background and key-visual images remain BACKGROUND media actions on
  the bottom layer; song text and name-tag stay text content elements.
- Wire the builder into ProExportService and ProBundleExportService (after
  generate) and split PlaylistExportService::writeProFile into
  generate -> inject -> write via ProFileWriter so the playlist path applies
  the same rule.
- Parser library unchanged.
- Migrate export tests to the combined rule via a shared InspectsSlideFillMedia
  support trait and add SlideMediaElementTest.
2026-07-07 22:15:12 +02:00

175 lines
5.9 KiB
PHP
Raw 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 Tests\Support;
use ProPresenter\Parser\Slide;
use Rv\Data\Action\ActionType;
use Rv\Data\Action\LayerType;
/**
* Test helpers for the COMBINED image rule:
*
* - BACKGROUND service image / key-visual → media ACTION on the BACKGROUND
* layer. Assert via the parser API: $slide->hasBackgroundMedia() and
* $slide->getBackgroundMediaUrl().
* - UPLOADED content slide image → full-frame fill.media CONTENT element.
* Assert via fillMediaUrlAt()/fillMediaUrls()/isFullFrameAt() and the
* foreground-action absence via hasForegroundImageMediaAction() == false.
* - Text (songtext / name-tag) → text CONTENT element (elementHasTextAt()).
*
* Content elements are full-frame Graphics\Element fills whose z-order is the
* base-slide element array order (index 0 = bottom layer): text first, the
* uploaded foreground image appended on top. The background image is NOT an
* element; it renders beneath the stack from its BACKGROUND media action.
*/
trait InspectsSlideFillMedia
{
/**
* Ordered base-slide content elements (z-order; index 0 = bottom layer).
*
* @return array<int, \Rv\Data\Slide\Element>
*/
protected function baseSlideElements(Slide $slide): array
{
foreach ($slide->getCue()->getActions() as $action) {
if ($action->getType() !== ActionType::ACTION_TYPE_PRESENTATION_SLIDE) {
continue;
}
$baseSlide = $action->getSlide()?->getPresentation()?->getBaseSlide();
if ($baseSlide === null) {
return [];
}
$elements = [];
foreach ($baseSlide->getElements() as $element) {
$elements[] = $element;
}
return $elements;
}
return [];
}
/** Bundle-relative fill-media URL of the element at $index, or null when it has no media fill. */
protected function fillMediaUrlAt(Slide $slide, int $index): ?string
{
$elements = $this->baseSlideElements($slide);
$graphics = ($elements[$index] ?? null)?->getElement();
$fill = $graphics?->getFill();
if ($fill === null || ! $fill->hasMedia()) {
return null;
}
return $fill->getMedia()?->getUrl()?->getAbsoluteString();
}
/**
* Bounds [x, y, width, height] of the element at $index, or null.
*
* @return array{0: float, 1: float, 2: float, 3: float}|null
*/
protected function fillBoundsAt(Slide $slide, int $index): ?array
{
$elements = $this->baseSlideElements($slide);
$bounds = ($elements[$index] ?? null)?->getElement()?->getBounds();
if ($bounds === null) {
return null;
}
return [
(float) ($bounds->getOrigin()?->getX() ?? 0.0),
(float) ($bounds->getOrigin()?->getY() ?? 0.0),
(float) ($bounds->getSize()?->getWidth() ?? 0.0),
(float) ($bounds->getSize()?->getHeight() ?? 0.0),
];
}
/** True when the element at $index is a full-frame (0,0 → 1920×1080) fill. */
protected function isFullFrameAt(Slide $slide, int $index): bool
{
return $this->fillBoundsAt($slide, $index) === [0.0, 0.0, 1920.0, 1080.0];
}
/**
* All fill-media URLs on the slide, in z-order (bottom → top).
*
* @return array<int, string>
*/
protected function fillMediaUrls(Slide $slide): array
{
$urls = [];
foreach ($this->baseSlideElements($slide) as $element) {
$fill = $element->getElement()?->getFill();
if ($fill !== null && $fill->hasMedia()) {
$urls[] = $fill->getMedia()?->getUrl()?->getAbsoluteString() ?? '';
}
}
return $urls;
}
/** Whether any base-slide element carries a fill-media image with the given URL. */
protected function hasFillMediaUrl(Slide $slide, string $url): bool
{
return in_array($url, $this->fillMediaUrls($slide), true);
}
/** True while the element at $index carries text (used to assert text sits above the background). */
protected function elementHasTextAt(Slide $slide, int $index): bool
{
$graphics = ($this->baseSlideElements($slide)[$index] ?? null)?->getElement();
return $graphics !== null && $graphics->hasText();
}
/**
* Whether the cue carries ANY image MEDIA action (either layer). Useful only
* when no service background is configured; when a background IS present it
* survives as a BACKGROUND action, so prefer hasForegroundImageMediaAction().
*/
protected function hasImageMediaAction(Slide $slide): bool
{
foreach ($slide->getCue()->getActions() as $action) {
if ($action->getType() !== ActionType::ACTION_TYPE_MEDIA) {
continue;
}
$mediaElement = $action->getMedia()?->getElement();
if ($mediaElement !== null && $mediaElement->hasImage()) {
return true;
}
}
return false;
}
/**
* Whether the cue still carries a FOREGROUND-layer image MEDIA action. Must be
* false after conversion — uploaded images become content elements. A
* BACKGROUND-layer action (service background / key-visual) is ignored here.
*/
protected function hasForegroundImageMediaAction(Slide $slide): bool
{
foreach ($slide->getCue()->getActions() as $action) {
if ($action->getType() !== ActionType::ACTION_TYPE_MEDIA) {
continue;
}
$mediaType = $action->getMedia();
$mediaElement = $mediaType?->getElement();
if (
$mediaElement !== null
&& $mediaElement->hasImage()
&& $mediaType->getLayerType() !== LayerType::LAYER_TYPE_BACKGROUND
) {
return true;
}
}
return false;
}
}