Compare commits
No commits in common. "20fe32af527b0d338e9921ffd17107000a8128c8" and "8c3ab65561a975fdcdf1670a1b87aa5fc904c7e9" have entirely different histories.
20fe32af52
...
8c3ab65561
|
|
@ -190,19 +190,14 @@ final class ProFileGenerator
|
|||
private static function buildCue(string $cueUuid, array $slideData): Cue
|
||||
{
|
||||
$elements = [];
|
||||
$imageOnly = ($slideData['imageOnly'] ?? false) === true;
|
||||
|
||||
if (! $imageOnly && isset($slideData['text'])) {
|
||||
if (isset($slideData['text'])) {
|
||||
$hasTranslation = isset($slideData['translation']) && $slideData['translation'] !== null;
|
||||
|
||||
if ($hasTranslation) {
|
||||
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text'], self::buildOriginalBounds());
|
||||
$elements[] = self::buildSlideElement('Deutsch', (string) $slideData['translation'], self::buildTranslationBounds());
|
||||
} else {
|
||||
$subtitle = isset($slideData['subtitle']) && $slideData['subtitle'] !== null
|
||||
? (string) $slideData['subtitle']
|
||||
: null;
|
||||
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text'], null, $subtitle);
|
||||
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,10 +220,6 @@ final class ProFileGenerator
|
|||
|
||||
$actions = [self::buildSlideAction($slideType)];
|
||||
|
||||
if (isset($slideData['background']) && is_array($slideData['background'])) {
|
||||
$actions[] = self::buildBackgroundMediaAction($slideData['background']);
|
||||
}
|
||||
|
||||
if (isset($slideData['media'])) {
|
||||
// Derive name from label OR filename without extension
|
||||
$mediaName = $slideData['label'] ?? null;
|
||||
|
|
@ -311,28 +302,8 @@ final class ProFileGenerator
|
|||
return $action;
|
||||
}
|
||||
|
||||
private static function buildBackgroundMediaAction(array $backgroundData): Action
|
||||
private static function buildMediaAction(string $absoluteUrl, string $format, ?string $name = null, int $imageWidth = 0, int $imageHeight = 0, bool $bundleRelative = false): Action
|
||||
{
|
||||
return self::buildMediaAction(
|
||||
(string) ($backgroundData['path'] ?? ''),
|
||||
(string) ($backgroundData['format'] ?? 'JPG'),
|
||||
null,
|
||||
(int) ($backgroundData['width'] ?? 1920),
|
||||
(int) ($backgroundData['height'] ?? 1080),
|
||||
(bool) ($backgroundData['bundleRelative'] ?? false),
|
||||
LayerType::LAYER_TYPE_BACKGROUND,
|
||||
);
|
||||
}
|
||||
|
||||
private static function buildMediaAction(
|
||||
string $absoluteUrl,
|
||||
string $format,
|
||||
?string $name = null,
|
||||
int $imageWidth = 0,
|
||||
int $imageHeight = 0,
|
||||
bool $bundleRelative = false,
|
||||
int $layerType = LayerType::LAYER_TYPE_FOREGROUND,
|
||||
): Action {
|
||||
if ($bundleRelative) {
|
||||
$filename = basename($absoluteUrl);
|
||||
$url = self::buildBundleRelativeUrl($filename);
|
||||
|
|
@ -384,7 +355,7 @@ final class ProFileGenerator
|
|||
$mediaElement->setImage($imageTypeProperties);
|
||||
|
||||
$mediaType = new MediaType();
|
||||
$mediaType->setLayerType($layerType);
|
||||
$mediaType->setLayerType(LayerType::LAYER_TYPE_FOREGROUND);
|
||||
$mediaType->setElement($mediaElement);
|
||||
$mediaType->setAudio(new Audio());
|
||||
|
||||
|
|
@ -415,7 +386,7 @@ final class ProFileGenerator
|
|||
return $url;
|
||||
}
|
||||
|
||||
private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null, ?string $subtitle = null): SlideElement
|
||||
private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null): SlideElement
|
||||
{
|
||||
$graphicsElement = new GraphicsElement();
|
||||
$graphicsElement->setUuid(self::newUuid());
|
||||
|
|
@ -429,7 +400,7 @@ final class ProFileGenerator
|
|||
$graphicsElement->setFeather(self::buildFeather());
|
||||
|
||||
$graphicsText = new Text();
|
||||
$graphicsText->setRtfData(self::buildRtf($text, $subtitle));
|
||||
$graphicsText->setRtfData(self::buildRtf($text));
|
||||
$graphicsText->setVerticalAlignment(VerticalAlignment::VERTICAL_ALIGNMENT_MIDDLE);
|
||||
$graphicsElement->setText($graphicsText);
|
||||
|
||||
|
|
@ -607,7 +578,6 @@ final class ProFileGenerator
|
|||
$color->setAlpha(1.0);
|
||||
$background = new Background();
|
||||
$background->setColor($color);
|
||||
|
||||
return $background;
|
||||
}
|
||||
|
||||
|
|
@ -615,7 +585,6 @@ final class ProFileGenerator
|
|||
{
|
||||
$url = new URL();
|
||||
$url->setPlatform(UrlPlatform::PLATFORM_MACOS);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +592,6 @@ final class ProFileGenerator
|
|||
{
|
||||
$timeline = new Timeline();
|
||||
$timeline->setDuration(300.0);
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
|
|
@ -674,23 +642,11 @@ final class ProFileGenerator
|
|||
$presentation->setCcli($metadata);
|
||||
}
|
||||
|
||||
private static function buildRtf(string $text, ?string $subtitle = null): string
|
||||
private static function buildRtf(string $text): string
|
||||
{
|
||||
$encodedText = self::encodePlainTextForRtf($text);
|
||||
|
||||
// Main text: \fs84. When a subtitle is provided, append it as a second
|
||||
// run on a new line that is explicitly non-bold (\b0) and smaller (\fs50,
|
||||
// ~60% of 84). Slides without a subtitle stay a single \fs84 run and the
|
||||
// exact byte output is identical to the previous single-run template.
|
||||
$body = '\fs84 \cf2 \CocoaLigature0 '.$encodedText;
|
||||
|
||||
if ($subtitle !== null && trim($subtitle) !== '') {
|
||||
$encodedSubtitle = self::encodePlainTextForRtf($subtitle);
|
||||
$body .= '\
|
||||
'.'\b0\fs50 '.$encodedSubtitle;
|
||||
}
|
||||
|
||||
return str_replace('BODY_HERE', $body, <<<'RTF'
|
||||
return str_replace('ENCODED_TEXT_HERE', $encodedText, <<<'RTF'
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2761
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;}
|
||||
{\colortbl;\red255\green255\blue255;\red255\green255\blue255;}
|
||||
|
|
@ -698,7 +654,7 @@ final class ProFileGenerator
|
|||
\deftab1680
|
||||
\pard\pardeftab1680\pardirnatural\qc\partightenfactor0
|
||||
|
||||
\f0BODY_HERE}
|
||||
\f0\fs84 \cf2 \CocoaLigature0 ENCODED_TEXT_HERE}
|
||||
RTF);
|
||||
}
|
||||
|
||||
|
|
@ -774,8 +730,8 @@ RTF);
|
|||
private static function newUuidString(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0F) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3F) | 0x80);
|
||||
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||
$hex = bin2hex($bytes);
|
||||
|
||||
return strtoupper(sprintf(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace ProPresenter\Parser;
|
|||
|
||||
use Rv\Data\Action;
|
||||
use Rv\Data\Action\ActionType;
|
||||
use Rv\Data\Action\LayerType;
|
||||
use Rv\Data\Action\MacroType;
|
||||
use Rv\Data\CollectionElementType;
|
||||
use Rv\Data\Cue;
|
||||
|
|
@ -119,7 +118,6 @@ class Slide
|
|||
public function getTranslation(): ?TextElement
|
||||
{
|
||||
$textElements = $this->getTextElements();
|
||||
|
||||
return $textElements[1] ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -151,28 +149,24 @@ class Slide
|
|||
public function getMacroName(): ?string
|
||||
{
|
||||
$macro = $this->findMacroAction();
|
||||
|
||||
return $macro?->getMacro()?->getIdentification()?->getParameterName();
|
||||
}
|
||||
|
||||
public function getMacroUuid(): ?string
|
||||
{
|
||||
$macro = $this->findMacroAction();
|
||||
|
||||
return $macro?->getMacro()?->getIdentification()?->getParameterUuid()?->getString();
|
||||
}
|
||||
|
||||
public function getMacroCollectionName(): ?string
|
||||
{
|
||||
$macro = $this->findMacroAction();
|
||||
|
||||
return $macro?->getMacro()?->getIdentification()?->getParentCollection()?->getParameterName();
|
||||
}
|
||||
|
||||
public function getMacroCollectionUuid(): ?string
|
||||
{
|
||||
$macro = $this->findMacroAction();
|
||||
|
||||
return $macro?->getMacro()?->getIdentification()?->getParentCollection()?->getParameterUuid()?->getString();
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +195,6 @@ class Slide
|
|||
$existingMacroAction->setType(ActionType::ACTION_TYPE_MACRO);
|
||||
$existingMacroAction->setMacro($macroType);
|
||||
$existingMacroAction->setIsEnabled(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -239,40 +232,18 @@ class Slide
|
|||
public function getMediaUrl(): ?string
|
||||
{
|
||||
$media = $this->findMediaAction();
|
||||
|
||||
return $media?->getMedia()?->getElement()?->getUrl()?->getAbsoluteString();
|
||||
}
|
||||
|
||||
public function getMediaUuid(): ?string
|
||||
{
|
||||
$media = $this->findMediaAction();
|
||||
|
||||
return $media?->getMedia()?->getElement()?->getUuid()?->getString();
|
||||
}
|
||||
|
||||
public function getMediaFormat(): ?string
|
||||
{
|
||||
$media = $this->findMediaAction();
|
||||
|
||||
return $media?->getMedia()?->getElement()?->getMetadata()?->getFormat();
|
||||
}
|
||||
|
||||
public function hasBackgroundMedia(): bool
|
||||
{
|
||||
return $this->findBackgroundMediaAction() !== null;
|
||||
}
|
||||
|
||||
public function getBackgroundMediaUrl(): ?string
|
||||
{
|
||||
$media = $this->findBackgroundMediaAction();
|
||||
|
||||
return $media?->getMedia()?->getElement()?->getUrl()?->getAbsoluteString();
|
||||
}
|
||||
|
||||
public function getBackgroundMediaFormat(): ?string
|
||||
{
|
||||
$media = $this->findBackgroundMediaAction();
|
||||
|
||||
return $media?->getMedia()?->getElement()?->getMetadata()?->getFormat();
|
||||
}
|
||||
|
||||
|
|
@ -333,19 +304,9 @@ class Slide
|
|||
}
|
||||
|
||||
private function findMediaAction(): ?Action
|
||||
{
|
||||
return $this->findMediaActionByLayerType(LayerType::LAYER_TYPE_FOREGROUND);
|
||||
}
|
||||
|
||||
private function findBackgroundMediaAction(): ?Action
|
||||
{
|
||||
return $this->findMediaActionByLayerType(LayerType::LAYER_TYPE_BACKGROUND);
|
||||
}
|
||||
|
||||
private function findMediaActionByLayerType(int $layerType): ?Action
|
||||
{
|
||||
foreach ($this->cue->getActions() as $action) {
|
||||
if ($action->getType() === ActionType::ACTION_TYPE_MEDIA && $action->getMedia()?->getLayerType() === $layerType) {
|
||||
if ($action->getType() === ActionType::ACTION_TYPE_MEDIA) {
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,140 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ProPresenter\Parser\ProFileGenerator;
|
||||
use Rv\Data\Action;
|
||||
use Rv\Data\Action\ActionType;
|
||||
use Rv\Data\Action\LayerType;
|
||||
|
||||
class BackgroundLayerTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function generated_text_slide_can_have_background_layer_media(): void
|
||||
{
|
||||
$song = ProFileGenerator::generate(
|
||||
'Background Text Song',
|
||||
[[
|
||||
'name' => 'Verse 1',
|
||||
'color' => [0.1, 0.2, 0.3, 1.0],
|
||||
'slides' => [[
|
||||
'text' => 'Text auf Folie',
|
||||
'background' => [
|
||||
'path' => 'file:///tmp/bg.jpg',
|
||||
'format' => 'JPG',
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
]],
|
||||
]],
|
||||
[['name' => 'normal', 'groupNames' => ['Verse 1']]],
|
||||
);
|
||||
|
||||
$slide = $song->getSlides()[0];
|
||||
$mediaActions = self::mediaActions($slide->getCue()->getActions());
|
||||
|
||||
$this->assertCount(1, $slide->getTextElements());
|
||||
$this->assertSame('Text auf Folie', $slide->getPlainText());
|
||||
$this->assertCount(1, $mediaActions);
|
||||
$this->assertSame(LayerType::LAYER_TYPE_BACKGROUND, $mediaActions[0]->getMedia()->getLayerType());
|
||||
$this->assertSame('file:///tmp/bg.jpg', $mediaActions[0]->getMedia()->getElement()->getUrl()->getAbsoluteString());
|
||||
$this->assertSame('JPG', $mediaActions[0]->getMedia()->getElement()->getMetadata()->getFormat());
|
||||
$this->assertSame(1920.0, $mediaActions[0]->getMedia()->getElement()->getImage()->getDrawing()->getNaturalSize()->getWidth());
|
||||
$this->assertSame(1080.0, $mediaActions[0]->getMedia()->getElement()->getImage()->getDrawing()->getNaturalSize()->getHeight());
|
||||
$this->assertTrue($slide->hasBackgroundMedia());
|
||||
$this->assertSame('file:///tmp/bg.jpg', $slide->getBackgroundMediaUrl());
|
||||
$this->assertSame('JPG', $slide->getBackgroundMediaFormat());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function generated_image_only_slide_has_no_text_elements(): void
|
||||
{
|
||||
$song = ProFileGenerator::generate(
|
||||
'Image Only Background Song',
|
||||
[[
|
||||
'name' => 'Image',
|
||||
'color' => [0.1, 0.2, 0.3, 1.0],
|
||||
'slides' => [[
|
||||
'text' => 'Dieser Text darf nicht erscheinen',
|
||||
'imageOnly' => true,
|
||||
'background' => [
|
||||
'path' => 'file:///tmp/image-only.jpg',
|
||||
'format' => 'JPG',
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
]],
|
||||
]],
|
||||
[['name' => 'normal', 'groupNames' => ['Image']]],
|
||||
);
|
||||
|
||||
$slide = $song->getSlides()[0];
|
||||
$mediaActions = self::mediaActions($slide->getCue()->getActions());
|
||||
|
||||
$this->assertCount(0, $slide->getTextElements());
|
||||
$this->assertSame('', $slide->getPlainText());
|
||||
$this->assertCount(1, $mediaActions);
|
||||
$this->assertSame(LayerType::LAYER_TYPE_BACKGROUND, $mediaActions[0]->getMedia()->getLayerType());
|
||||
$this->assertTrue($slide->hasBackgroundMedia());
|
||||
$this->assertSame('file:///tmp/image-only.jpg', $slide->getBackgroundMediaUrl());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function generated_slide_can_have_background_and_foreground_media_in_order(): void
|
||||
{
|
||||
$song = ProFileGenerator::generate(
|
||||
'Mixed Media Song',
|
||||
[[
|
||||
'name' => 'Verse 1',
|
||||
'color' => [0.1, 0.2, 0.3, 1.0],
|
||||
'slides' => [[
|
||||
'text' => 'Text vor Bild',
|
||||
'background' => [
|
||||
'path' => 'file:///tmp/background.jpg',
|
||||
'format' => 'JPG',
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
],
|
||||
'media' => 'file:///tmp/foreground.png',
|
||||
'format' => 'PNG',
|
||||
'mediaWidth' => 640,
|
||||
'mediaHeight' => 360,
|
||||
]],
|
||||
]],
|
||||
[['name' => 'normal', 'groupNames' => ['Verse 1']]],
|
||||
);
|
||||
|
||||
$slide = $song->getSlides()[0];
|
||||
$mediaActions = self::mediaActions($slide->getCue()->getActions());
|
||||
|
||||
$this->assertCount(2, $mediaActions);
|
||||
$this->assertSame(LayerType::LAYER_TYPE_BACKGROUND, $mediaActions[0]->getMedia()->getLayerType());
|
||||
$this->assertSame(LayerType::LAYER_TYPE_FOREGROUND, $mediaActions[1]->getMedia()->getLayerType());
|
||||
$this->assertSame('file:///tmp/background.jpg', $mediaActions[0]->getMedia()->getElement()->getUrl()->getAbsoluteString());
|
||||
$this->assertSame('file:///tmp/foreground.png', $mediaActions[1]->getMedia()->getElement()->getUrl()->getAbsoluteString());
|
||||
$this->assertTrue($slide->hasBackgroundMedia());
|
||||
$this->assertTrue($slide->hasMedia());
|
||||
$this->assertSame('file:///tmp/background.jpg', $slide->getBackgroundMediaUrl());
|
||||
$this->assertSame('file:///tmp/foreground.png', $slide->getMediaUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<Action> $actions
|
||||
* @return Action[]
|
||||
*/
|
||||
private static function mediaActions(iterable $actions): array
|
||||
{
|
||||
$mediaActions = [];
|
||||
foreach ($actions as $action) {
|
||||
if ($action->getType() === ActionType::ACTION_TYPE_MEDIA) {
|
||||
$mediaActions[] = $action;
|
||||
}
|
||||
}
|
||||
|
||||
return $mediaActions;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ProPresenter\Parser\ProFileGenerator;
|
||||
|
||||
final class NameTagSubtitleRtfTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Pull the RTF data of the first slide's first text element.
|
||||
*/
|
||||
private function firstSlideRtf(array $slideData): string
|
||||
{
|
||||
$song = ProFileGenerator::generate(
|
||||
'Subtitle Test',
|
||||
[
|
||||
[
|
||||
'name' => 'V1',
|
||||
'color' => [0, 0, 0, 1],
|
||||
'slides' => [$slideData],
|
||||
],
|
||||
],
|
||||
[
|
||||
['name' => 'normal', 'groupNames' => ['V1']],
|
||||
],
|
||||
);
|
||||
|
||||
$cue = $song->getPresentation()->getCues()[0];
|
||||
$baseSlide = $cue->getActions()[0]->getSlide()->getPresentation()->getBaseSlide();
|
||||
$elements = $baseSlide->getElements();
|
||||
|
||||
$this->assertNotEmpty($elements, 'Slide must have at least one element');
|
||||
|
||||
return $elements[0]->getElement()->getText()->getRtfData();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function subtitle_renders_as_separate_non_bold_smaller_run(): void
|
||||
{
|
||||
$rtf = $this->firstSlideRtf([
|
||||
'text' => 'Max Mustermann',
|
||||
'subtitle' => 'Moderation',
|
||||
]);
|
||||
|
||||
// Main text keeps \fs84.
|
||||
$this->assertStringContainsString('\fs84', $rtf);
|
||||
$this->assertStringContainsString('Max Mustermann', $rtf);
|
||||
|
||||
// Subtitle is a separate run that is explicitly non-bold (\b0) and smaller (\fs50).
|
||||
$this->assertStringContainsString('\b0\fs50', $rtf);
|
||||
$this->assertStringContainsString('Moderation', $rtf);
|
||||
|
||||
// The subtitle run must come after the main text.
|
||||
$this->assertLessThan(
|
||||
strpos($rtf, '\b0\fs50'),
|
||||
strpos($rtf, 'Max Mustermann'),
|
||||
'Main text must precede the subtitle run',
|
||||
);
|
||||
|
||||
// The subtitle run must follow the main \fs84 run.
|
||||
$this->assertLessThan(
|
||||
strpos($rtf, '\b0\fs50'),
|
||||
strpos($rtf, '\fs84'),
|
||||
'\fs84 main run must precede the \fs50 subtitle run',
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function slide_without_subtitle_is_unchanged_single_run(): void
|
||||
{
|
||||
$rtf = $this->firstSlideRtf([
|
||||
'text' => 'Max Mustermann',
|
||||
]);
|
||||
|
||||
// Exact single-run body identical to the legacy output.
|
||||
$this->assertStringContainsString(
|
||||
'\f0\fs84 \cf2 \CocoaLigature0 Max Mustermann',
|
||||
$rtf,
|
||||
);
|
||||
|
||||
// No subtitle styling leaks into lyric/plain slides.
|
||||
$this->assertStringNotContainsString('\b0', $rtf);
|
||||
$this->assertStringNotContainsString('\fs50', $rtf);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function empty_subtitle_is_treated_as_no_subtitle(): void
|
||||
{
|
||||
$rtf = $this->firstSlideRtf([
|
||||
'text' => 'Max Mustermann',
|
||||
'subtitle' => ' ',
|
||||
]);
|
||||
|
||||
$this->assertStringNotContainsString('\b0', $rtf);
|
||||
$this->assertStringNotContainsString('\fs50', $rtf);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue