feat(generator): render nametag subtitle as smaller non-bold RTF run
- subtitle now emitted as second run (\b0\fs50); main text unchanged (\fs84) - slides without subtitle byte-identical to previous single-run output - add NameTagSubtitleRtfTest
This commit is contained in:
parent
582ef85c2a
commit
d187b092e3
|
|
@ -189,7 +189,10 @@ final class ProFileGenerator
|
||||||
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text'], self::buildOriginalBounds());
|
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text'], self::buildOriginalBounds());
|
||||||
$elements[] = self::buildSlideElement('Deutsch', (string) $slideData['translation'], self::buildTranslationBounds());
|
$elements[] = self::buildSlideElement('Deutsch', (string) $slideData['translation'], self::buildTranslationBounds());
|
||||||
} else {
|
} else {
|
||||||
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text']);
|
$subtitle = isset($slideData['subtitle']) && $slideData['subtitle'] !== null
|
||||||
|
? (string) $slideData['subtitle']
|
||||||
|
: null;
|
||||||
|
$elements[] = self::buildSlideElement('Orginal', (string) $slideData['text'], null, $subtitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,7 +395,7 @@ final class ProFileGenerator
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null): SlideElement
|
private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null, ?string $subtitle = null): SlideElement
|
||||||
{
|
{
|
||||||
$graphicsElement = new GraphicsElement();
|
$graphicsElement = new GraphicsElement();
|
||||||
$graphicsElement->setUuid(self::newUuid());
|
$graphicsElement->setUuid(self::newUuid());
|
||||||
|
|
@ -406,7 +409,7 @@ final class ProFileGenerator
|
||||||
$graphicsElement->setFeather(self::buildFeather());
|
$graphicsElement->setFeather(self::buildFeather());
|
||||||
|
|
||||||
$graphicsText = new Text();
|
$graphicsText = new Text();
|
||||||
$graphicsText->setRtfData(self::buildRtf($text));
|
$graphicsText->setRtfData(self::buildRtf($text, $subtitle));
|
||||||
$graphicsText->setVerticalAlignment(VerticalAlignment::VERTICAL_ALIGNMENT_MIDDLE);
|
$graphicsText->setVerticalAlignment(VerticalAlignment::VERTICAL_ALIGNMENT_MIDDLE);
|
||||||
$graphicsElement->setText($graphicsText);
|
$graphicsElement->setText($graphicsText);
|
||||||
|
|
||||||
|
|
@ -635,11 +638,23 @@ final class ProFileGenerator
|
||||||
$presentation->setCcli($metadata);
|
$presentation->setCcli($metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function buildRtf(string $text): string
|
private static function buildRtf(string $text, ?string $subtitle = null): string
|
||||||
{
|
{
|
||||||
$encodedText = self::encodePlainTextForRtf($text);
|
$encodedText = self::encodePlainTextForRtf($text);
|
||||||
|
|
||||||
return str_replace('ENCODED_TEXT_HERE', $encodedText, <<<'RTF'
|
// 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'
|
||||||
{\rtf1\ansi\ansicpg1252\cocoartf2761
|
{\rtf1\ansi\ansicpg1252\cocoartf2761
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;}
|
||||||
{\colortbl;\red255\green255\blue255;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;\red255\green255\blue255;}
|
||||||
|
|
@ -647,7 +662,7 @@ final class ProFileGenerator
|
||||||
\deftab1680
|
\deftab1680
|
||||||
\pard\pardeftab1680\pardirnatural\qc\partightenfactor0
|
\pard\pardeftab1680\pardirnatural\qc\partightenfactor0
|
||||||
|
|
||||||
\f0\fs84 \cf2 \CocoaLigature0 ENCODED_TEXT_HERE}
|
\f0BODY_HERE}
|
||||||
RTF);
|
RTF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
101
tests/NameTagSubtitleRtfTest.php
Normal file
101
tests/NameTagSubtitleRtfTest.php
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?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