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('Deutsch', (string) $slideData['translation'], self::buildTranslationBounds());
|
||||
} 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;
|
||||
}
|
||||
|
||||
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->setUuid(self::newUuid());
|
||||
|
|
@ -406,7 +409,7 @@ final class ProFileGenerator
|
|||
$graphicsElement->setFeather(self::buildFeather());
|
||||
|
||||
$graphicsText = new Text();
|
||||
$graphicsText->setRtfData(self::buildRtf($text));
|
||||
$graphicsText->setRtfData(self::buildRtf($text, $subtitle));
|
||||
$graphicsText->setVerticalAlignment(VerticalAlignment::VERTICAL_ALIGNMENT_MIDDLE);
|
||||
$graphicsElement->setText($graphicsText);
|
||||
|
||||
|
|
@ -635,11 +638,23 @@ final class ProFileGenerator
|
|||
$presentation->setCcli($metadata);
|
||||
}
|
||||
|
||||
private static function buildRtf(string $text): string
|
||||
private static function buildRtf(string $text, ?string $subtitle = null): string
|
||||
{
|
||||
$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
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;}
|
||||
{\colortbl;\red255\green255\blue255;\red255\green255\blue255;}
|
||||
|
|
@ -647,7 +662,7 @@ final class ProFileGenerator
|
|||
\deftab1680
|
||||
\pard\pardeftab1680\pardirnatural\qc\partightenfactor0
|
||||
|
||||
\f0\fs84 \cf2 \CocoaLigature0 ENCODED_TEXT_HERE}
|
||||
\f0BODY_HERE}
|
||||
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