- 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.
175 lines
5.9 KiB
PHP
175 lines
5.9 KiB
PHP
<?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;
|
||
}
|
||
}
|