feat(library): add readers + writers for all ProPresenter global libraries and theme bundles

Add full IO support for every global ProPresenter library file plus
theme folders, and extend the existing Labels/Macros readers with
exporters and editable accessors so every supported document is now a
round-trippable, mutable object.

New library readers/writers (each: FileReader, FileWriter, Library
wrapper, element wrapper where applicable, CLI tool, tests, doc/api/*.md):

- Groups          (ProGroupsDocument)        + GroupDefinition
- ClearGroups     (ClearGroupsDocument)      + ClearGroupDefinition
- CCLI            (CCLIDocument)
- Messages        (MessageDocument)          + Message
- Timers          (TimersDocument + Clock)   + Timer
- Stage           (Stage.Document)           + StageLayout
- Workspace       (ProPresenterWorkspace)    + Screen
- Props           (PropDocument)             + Prop
- TestPatterns    (TestPatternDocument)
- Calendar        (new CalendarDocument)     + CalendarEvent
- KeyMappings     (new KeyMappingsDocument)  + KeyMapping
- CommunicationDevices (JSON file)           + CommunicationDevice
- Theme bundles   (Template.Document folder + Assets/) + ThemeBundle/Slide/Asset

Extensions to existing modules:

- LabelsFileWriter; Label and LabelLibrary gain setters, addLabel,
  removeLabel, setColor / setColorHex helpers
- MacrosFileWriter; Macro/MacroCollection/MacroLibrary gain UUID, name,
  color, image_type, image_data, trigger_on_startup setters plus
  add/remove for macros and collections

Two new minimal proto schemas were defined for documents that lacked
upstream definitions:

- proto/calendar.proto   - CalendarDocument with Event entries, raw
  bytes for the action/macro sub-messages so the schema can evolve
- proto/keyMappings.proto - KeyMappingsDocument with ApplicationInfo
  and a forward-looking Mapping message (sample only carries the info)

The Theme file turned out to be a regular Rv\Data\Template\Document, so
no new proto was required for theme content; ThemeBundle layers folder
+ Assets/ handling on top in the same spirit as PresentationBundle.

GroupDefinition is intentionally distinct from the existing Group class
(which wraps song-level CueGroup) to avoid breaking song APIs.

Verified with the full PHPUnit suite: 370 tests, 9200 assertions, all
green; LSP diagnostics clean across src/. The unmodified reference
samples for Labels, Groups, ClearGroups, TestPatterns, Calendar and
KeyMappings round-trip byte-for-byte; the others round-trip with the
same byte length (PHP protobuf is not canonically deterministic but
re-write-after-write stabilises).

doc/INDEX.md, doc/keywords.md and AGENTS.md updated so every new module
is discoverable from the top level.
This commit is contained in:
Thorsten Bus 2026-05-03 21:40:09 +02:00
parent 4e1ac9b7ea
commit 9e3e719806
142 changed files with 8557 additions and 209 deletions

View file

@ -19,8 +19,21 @@ All project documentation lives in `doc/`. Load only what you need.
| Parse/modify `.pro` song files | `doc/api/song.md` | | Parse/modify `.pro` song files | `doc/api/song.md` |
| Parse/modify `.proplaylist` files | `doc/api/playlist.md` | | Parse/modify `.proplaylist` files | `doc/api/playlist.md` |
| Parse/modify `.probundle` files | `doc/api/bundle.md` | | Parse/modify `.probundle` files | `doc/api/bundle.md` |
| Read the global `Macros` file | `doc/api/macros.md` | | Read/write the global `Macros` file | `doc/api/macros.md` |
| Read the global `Labels` file | `doc/api/labels.md` | | Read/write the global `Labels` file | `doc/api/labels.md` |
| Read/write the global `Groups` file | `doc/api/groups.md` |
| Read/write the global `ClearGroups` file | `doc/api/clear-groups.md` |
| Read/write the global `CCLI` file | `doc/api/ccli.md` |
| Read/write the global `Messages` file | `doc/api/messages.md` |
| Read/write the global `Timers` file | `doc/api/timers.md` |
| Read/write the global `Stage` file | `doc/api/stage.md` |
| Read/write the global `Workspace` file | `doc/api/workspace.md` |
| Read/write the global `Props` file | `doc/api/props.md` |
| Read/write the global `TestPatterns` file | `doc/api/test-patterns.md` |
| Read/write the global `Calendar` file | `doc/api/calendar.md` |
| Read/write the global `KeyMappings` file | `doc/api/key-mappings.md` |
| Read/write the `CommunicationDevices` JSON file | `doc/api/communication-devices.md` |
| Read/write a theme folder (Theme + Assets/) | `doc/api/theme.md` |
| Understand `.pro` binary format | `doc/formats/pp_song_spec.md` | | Understand `.pro` binary format | `doc/formats/pp_song_spec.md` |
| Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` | | Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` |
| Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` | | Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` |
@ -57,16 +70,34 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files:
- **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations - **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations
- **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs - **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs
- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets - **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets
- **Macros** (`Macros`, no extension) — Global protobuf-encoded macro library with collections - **Themes** (folder with `Theme` + `Assets/`) — Template document plus media used as a slide theme
- **Labels** (`Labels`, no extension) — Global protobuf-encoded label library (slide labels with optional UI colors) - **Global library files** (no extension) — `Macros`, `Labels`, `Groups`, `ClearGroups`, `CCLI`, `Messages`, `Timers`, `Stage`, `Workspace`, `Props`, `TestPatterns`, `Calendar`, `KeyMappings` (protobuf) and `CommunicationDevices` (JSON)
### CLI Tools ### CLI Tools
```bash ```bash
# Songs / playlists / bundles
php bin/parse-song.php path/to/song.pro php bin/parse-song.php path/to/song.pro
php bin/parse-playlist.php path/to/playlist.proplaylist php bin/parse-playlist.php path/to/playlist.proplaylist
# Global library files (one parser per type)
php bin/parse-macros.php path/to/Macros php bin/parse-macros.php path/to/Macros
php bin/parse-labels.php path/to/Labels php bin/parse-labels.php path/to/Labels
php bin/parse-groups.php path/to/Groups
php bin/parse-clear-groups.php path/to/ClearGroups
php bin/parse-ccli.php path/to/CCLI
php bin/parse-messages.php path/to/Messages
php bin/parse-timers.php path/to/Timers
php bin/parse-stage.php path/to/Stage
php bin/parse-workspace.php path/to/Workspace
php bin/parse-props.php path/to/Props
php bin/parse-test-patterns.php path/to/TestPatterns
php bin/parse-calendar.php path/to/Calendar
php bin/parse-key-mappings.php path/to/KeyMappings
php bin/parse-communication-devices.php path/to/CommunicationDevices
# Theme folder
php bin/parse-theme.php path/to/ThemeFolder
``` ```
### Key Source Files ### Key Source Files

26
bin/parse-calendar.php Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\CalendarFileReader;
if ($argc < 2) {
echo "Usage: parse-calendar.php <Calendar>\n";
exit(1);
}
try {
$library = CalendarFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
$events = $library->getEvents();
echo 'Calendar events (' . count($events) . ') mode=' . $library->getMode() . ":\n";
foreach ($events as $index => $event) {
echo sprintf(" [%d] %s :: %s :: start=%s :: end=%s :: action=%dB :: macro=%dB\n", $index + 1, $event->getName() === '' ? '(unnamed)' : $event->getName(), $event->getUuid(), (string) ($event->getStartTimeSeconds() ?? ''), (string) ($event->getEndTimeSeconds() ?? ''), strlen($event->getActionData()), strlen($event->getMacroData()));
}

25
bin/parse-ccli.php Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\CCLIFileReader;
if ($argc < 2) {
echo "Usage: parse-ccli.php <CCLI>\n";
exit(1);
}
$filePath = $argv[1];
try {
$library = CCLIFileReader::read($filePath);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
echo "CCLI (1):\n";
echo sprintf(" [1] enabled=%s :: license=%s :: display_type=%d :: template=%s\n", $library->isCCLIDisplayEnabled() ? 'yes' : 'no', $library->getCCLILicense() === '' ? '(empty)' : $library->getCCLILicense(), $library->getDisplayType(), $library->getTemplate() === null ? 'no' : 'yes');

View file

@ -0,0 +1,35 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\ClearGroupsFileReader;
if ($argc < 2) {
echo "Usage: parse-clear-groups.php <ClearGroups>\n";
exit(1);
}
$filePath = $argv[1];
try {
$library = ClearGroupsFileReader::read($filePath);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
$groups = $library->getGroups();
echo "ClearGroups (" . count($groups) . "):\n";
foreach ($groups as $index => $group) {
$number = $index + 1;
$name = $group->getName();
$displayName = $name === '' ? '(unnamed)' : $name;
$uuid = $group->getUuid();
$colorPart = $group->getColorHex() ?? '(no tint)';
echo sprintf(" [%d] %s :: %s :: image_type=%d :: %s\n", $number, $displayName, $uuid, $group->getImageType(), $colorPart);
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\CommunicationDevicesFileReader;
if ($argc < 2) {
echo "Usage: parse-communication-devices.php <CommunicationDevices>\n";
exit(1);
}
try {
$library = CommunicationDevicesFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
$devices = $library->getDevices();
echo 'Communication devices (' . count($devices) . "):\n";
if ($devices === []) {
echo " (none configured)\n";
}
foreach ($devices as $index => $device) {
echo sprintf(" [%d] %s :: %s :: %s :: %s\n", $index + 1, $device->getName() === '' ? '(unnamed)' : $device->getName(), $device->getId(), $device->getType(), $device->getAddress());
}

35
bin/parse-groups.php Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\GroupsFileReader;
if ($argc < 2) {
echo "Usage: parse-groups.php <Groups>\n";
exit(1);
}
$filePath = $argv[1];
try {
$library = GroupsFileReader::read($filePath);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
$groups = $library->getGroups();
echo "Groups (" . count($groups) . "):\n";
foreach ($groups as $index => $group) {
$number = $index + 1;
$name = $group->getName();
$displayName = $name === '' ? '(unnamed)' : $name;
$uuid = $group->getUuid();
$colorPart = $group->getColorHex() ?? '(no color)';
echo sprintf(" [%d] %s :: %s :: %s\n", $number, $displayName, $uuid, $colorPart);
}

View file

@ -0,0 +1,35 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\KeyMappingsFileReader;
if ($argc < 2) {
echo "Usage: parse-key-mappings.php <KeyMappings>\n";
exit(1);
}
$filePath = $argv[1];
try {
$library = KeyMappingsFileReader::read($filePath);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
$mappings = $library->getMappings();
echo "KeyMappings (" . count($mappings) . "):\n";
foreach ($mappings as $index => $mapping) {
$number = $index + 1;
$name = $mapping->getName();
$displayName = $name === '' ? '(unnamed)' : $name;
$uuid = $mapping->getUuid();
$hotKeyPart = $mapping->getHotKey() === null ? '(no hot key)' : 'hot_key=yes';
echo sprintf(" [%d] %s :: %s :: target_bytes=%d :: %s\n", $number, $displayName, $uuid, strlen($mapping->getTarget()), $hotKeyPart);
}

27
bin/parse-messages.php Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\MessagesFileReader;
if ($argc < 2) {
echo "Usage: parse-messages.php <Messages>\n";
exit(1);
}
try {
$library = MessagesFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
$messages = $library->getMessages();
echo 'Messages (' . count($messages) . "):\n";
foreach ($messages as $index => $message) {
$title = $message->getTitle() === '' ? '(untitled)' : $message->getTitle();
echo sprintf(" [%d] %s :: %s :: clear=%d :: network=%s\n", $index + 1, $title, $message->getUuid(), $message->getClearType(), $message->isVisibleOnNetwork() ? 'yes' : 'no');
}

25
bin/parse-props.php Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\PropsFileReader;
if ($argc < 2) {
echo "Usage: parse-props.php <Props>\n";
exit(1);
}
try {
$library = PropsFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
echo 'Props (' . $library->count() . "):\n";
foreach ($library->getProps() as $index => $prop) {
echo sprintf(" [%d] %s :: %s :: %s\n", $index + 1, $prop->getName() ?: '(unnamed)', $prop->getUuid(), $prop->isEnabled() ? 'enabled' : 'disabled');
}

25
bin/parse-stage.php Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\StageFileReader;
if ($argc < 2) {
echo "Usage: parse-stage.php <Stage>\n";
exit(1);
}
try {
$library = StageFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
echo 'Stage layouts (' . $library->count() . "):\n";
foreach ($library->getLayouts() as $index => $layout) {
echo sprintf(" [%d] %s :: %s\n", $index + 1, $layout->getName() ?: '(unnamed)', $layout->getUuid());
}

View file

@ -0,0 +1,34 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\TestPatternsFileReader;
if ($argc < 2) {
echo "Usage: parse-test-patterns.php <TestPatterns>\n";
exit(1);
}
$filePath = $argv[1];
try {
$library = TestPatternsFileReader::read($filePath);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
echo "TestPatterns (" . $library->count() . "):\n";
echo sprintf(" State: selected=%s :: name=%s :: display_location=%d :: screen=%s\n", $library->getSelectedPatternUuid() === '' ? '(none)' : $library->getSelectedPatternUuid(), $library->getSelectedPatternNameLocalizationKey() === '' ? '(none)' : $library->getSelectedPatternNameLocalizationKey(), $library->getDisplayLocation(), $library->getSpecificScreenUuid() === '' ? '(none)' : $library->getSpecificScreenUuid());
foreach ($library->getPatterns() as $index => $pattern) {
$number = $index + 1;
$name = $pattern->getNameLocalizationKey();
$displayName = $name === '' ? '(unnamed)' : $name;
$uuid = $pattern->getUuid()?->getString() ?? '';
echo sprintf(" [%d] %s :: %s\n", $number, $displayName, $uuid);
}

30
bin/parse-theme.php Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\ThemeFileReader;
if ($argc < 2) {
echo "Usage: parse-theme.php <theme-folder>\n";
exit(1);
}
try {
$bundle = ThemeFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
echo "Theme folder: {$argv[1]}\n";
echo 'Slides (' . $bundle->count() . "):\n";
foreach ($bundle->getSlides() as $index => $slide) {
echo sprintf(" [%d] %s\n", $index + 1, $slide->getName() ?: '(unnamed)');
}
echo 'Assets (' . $bundle->getAssetCount() . "):\n";
foreach ($bundle->getAssets() as $index => $asset) {
echo sprintf(" [%d] Assets/%s :: %d bytes :: %s\n", $index + 1, $asset->getName(), $asset->getSize(), $asset->getMimeType());
}

29
bin/parse-timers.php Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\TimersFileReader;
if ($argc < 2) {
echo "Usage: parse-timers.php <Timers>\n";
exit(1);
}
try {
$library = TimersFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
$timers = $library->getTimers();
echo 'Timers (' . count($timers) . ') clock=' . $library->getClockFormat() . ":\n";
foreach ($timers as $index => $timer) {
$type = $timer->isCountdown() ? 'countdown' : ($timer->isCountdownToTime() ? 'countdown_to_time' : ($timer->isElapsedTime() ? 'elapsed_time' : 'unknown'));
$duration = $timer->getDurationSeconds();
$durationPart = $duration === null ? '' : sprintf(' :: %ds', $duration);
echo sprintf(" [%d] %s :: %s :: %s%s\n", $index + 1, $timer->getName() === '' ? '(unnamed)' : $timer->getName(), $timer->getUuid(), $type, $durationPart);
}

25
bin/parse-workspace.php Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use ProPresenter\Parser\WorkspaceFileReader;
if ($argc < 2) {
echo "Usage: parse-workspace.php <Workspace>\n";
exit(1);
}
try {
$library = WorkspaceFileReader::read($argv[1]);
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
echo 'Screens (' . $library->count() . "):\n";
foreach ($library->getScreens() as $index => $screen) {
echo sprintf(" [%d] %s :: %s :: type %d\n", $index + 1, $screen->getName() ?: '(unnamed)', $screen->getUuid(), $screen->getScreenType());
}

View file

@ -9,8 +9,21 @@
| Parse/modify `.pro` song files | [api/song.md](api/song.md) | | Parse/modify `.pro` song files | [api/song.md](api/song.md) |
| Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) | | Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) |
| Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) | | Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) |
| Read the global `Macros` file | [api/macros.md](api/macros.md) | | Read/write the global `Macros` file | [api/macros.md](api/macros.md) |
| Read the global `Labels` file | [api/labels.md](api/labels.md) | | Read/write the global `Labels` file | [api/labels.md](api/labels.md) |
| Read/write the global `Groups` file | [api/groups.md](api/groups.md) |
| Read/write the global `ClearGroups` file | [api/clear-groups.md](api/clear-groups.md) |
| Read/write the global `CCLI` file | [api/ccli.md](api/ccli.md) |
| Read/write the global `Messages` file | [api/messages.md](api/messages.md) |
| Read/write the global `Timers` file | [api/timers.md](api/timers.md) |
| Read/write the global `Stage` file | [api/stage.md](api/stage.md) |
| Read/write the global `Workspace` file | [api/workspace.md](api/workspace.md) |
| Read/write the global `Props` file | [api/props.md](api/props.md) |
| Read/write the global `TestPatterns` file | [api/test-patterns.md](api/test-patterns.md) |
| Read/write the global `Calendar` file | [api/calendar.md](api/calendar.md) |
| Read/write the global `KeyMappings` file | [api/key-mappings.md](api/key-mappings.md) |
| Read/write the global `CommunicationDevices` JSON file | [api/communication-devices.md](api/communication-devices.md) |
| Read/write theme folders (Theme + Assets/) | [api/theme.md](api/theme.md) |
| Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) | | Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
| Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
@ -27,11 +40,27 @@
- [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) — ProPresenter 7 `.probundle` file format (ZIP container, media assets) - [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) — ProPresenter 7 `.probundle` file format (ZIP container, media assets)
### PHP API Documentation ### PHP API Documentation
#### Document containers
- [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files) - [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files)
- [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files) - [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files)
- [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media) - [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media)
- [api/macros.md](api/macros.md) — Macros library API (read the global `Macros` file) - [api/theme.md](api/theme.md) — Theme bundle API (folder with `Theme` proto + `Assets/`)
- [api/labels.md](api/labels.md) — Labels library API (read the global `Labels` file)
#### Global library files (read + write)
- [api/macros.md](api/macros.md) — `Macros` library (macros + collections, with editable accessors)
- [api/labels.md](api/labels.md) — `Labels` library (named slide labels with optional UI colors)
- [api/groups.md](api/groups.md) — `Groups` library (named library groups with UUID, color, hot keys)
- [api/clear-groups.md](api/clear-groups.md) — `ClearGroups` library (clear-action groups)
- [api/ccli.md](api/ccli.md) — `CCLI` settings (license, display behaviour, copyright template)
- [api/messages.md](api/messages.md) — `Messages` library (lower-third / overlay messages)
- [api/timers.md](api/timers.md) — `Timers` library (timer definitions + clock format)
- [api/stage.md](api/stage.md) — `Stage` document (stage display layouts)
- [api/workspace.md](api/workspace.md) — `Workspace` document (screens, looks, masks, audio/video inputs)
- [api/props.md](api/props.md) — `Props` document (prop cues + transition)
- [api/test-patterns.md](api/test-patterns.md) — `TestPatterns` document (currently selected pattern + saved overrides)
- [api/calendar.md](api/calendar.md) — `Calendar` document (scheduled events firing macros)
- [api/key-mappings.md](api/key-mappings.md) — `KeyMappings` document (custom hot-key bindings)
- [api/communication-devices.md](api/communication-devices.md) — `CommunicationDevices` JSON list (MIDI / serial / OSC bindings)
### Internal Reference ### Internal Reference
- [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered - [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered
@ -59,8 +88,21 @@ doc/
│ ├── song.md │ ├── song.md
│ ├── playlist.md │ ├── playlist.md
│ ├── bundle.md │ ├── bundle.md
│ ├── theme.md
│ ├── macros.md │ ├── macros.md
│ └── labels.md │ ├── labels.md
│ ├── groups.md
│ ├── clear-groups.md
│ ├── ccli.md
│ ├── messages.md
│ ├── timers.md
│ ├── stage.md
│ ├── workspace.md
│ ├── props.md
│ ├── test-patterns.md
│ ├── calendar.md
│ ├── key-mappings.md
│ └── communication-devices.md
└── internal/ ← Development notes (optional context) └── internal/ ← Development notes (optional context)
├── learnings.md ├── learnings.md
├── decisions.md ├── decisions.md
@ -86,6 +128,17 @@ Load: doc/api/playlist.md
Load: doc/api/bundle.md Load: doc/api/bundle.md
``` ```
### Task: "Edit a global library file (Macros, Labels, Groups, etc.)"
```
Load: doc/api/<library>.md
```
### Task: "Round-trip a Theme folder with assets"
```
Load: doc/api/theme.md
Load: doc/api/bundle.md (for the bundle pattern reference)
```
### Task: "Debug protobuf parsing issues" ### Task: "Debug protobuf parsing issues"
``` ```
Load: doc/formats/pp_song_spec.md (sections 2-5) Load: doc/formats/pp_song_spec.md (sections 2-5)
@ -113,6 +166,8 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
- **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations - **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations
- **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files - **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files
- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets - **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets
- **Global library files**`Macros`, `Labels`, `Groups`, `ClearGroups`, `CCLI`, `Messages`, `Timers`, `Stage`, `Workspace`, `Props`, `TestPatterns`, `Calendar`, `KeyMappings`, `CommunicationDevices` (JSON)
- **Theme folders** — directory with a `Theme` protobuf file plus an `Assets/` subdirectory of media
### Key Components ### Key Components
@ -129,23 +184,46 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
| `src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) | | `src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) |
| `src/ProBundleReader.php` | Read `.probundle` files | | `src/ProBundleReader.php` | Read `.probundle` files |
| `src/ProBundleWriter.php` | Write `.probundle` files | | `src/ProBundleWriter.php` | Write `.probundle` files |
| `src/MacroLibrary.php` | Macros library wrapper (read global `Macros` file) | | `src/ThemeBundle.php` | Theme folder wrapper (Template.Document + Assets/) |
| `src/MacrosFileReader.php` | Read global `Macros` file | | `src/ThemeFileReader.php` / `ThemeFileWriter.php` | Read/write theme folders |
| `src/LabelLibrary.php` | Labels library wrapper (read global `Labels` file) | | `src/MacroLibrary.php` / `MacrosFileReader.php` / `MacrosFileWriter.php` | Macros file IO |
| `src/LabelsFileReader.php` | Read global `Labels` file | | `src/LabelLibrary.php` / `LabelsFileReader.php` / `LabelsFileWriter.php` | Labels file IO |
| `src/GroupLibrary.php` / `GroupsFileReader.php` / `GroupsFileWriter.php` | Groups file IO |
| `src/ClearGroupsLibrary.php` / `ClearGroupsFileReader.php` / `ClearGroupsFileWriter.php` | ClearGroups file IO |
| `src/CCLILibrary.php` / `CCLIFileReader.php` / `CCLIFileWriter.php` | CCLI file IO |
| `src/MessageLibrary.php` / `MessagesFileReader.php` / `MessagesFileWriter.php` | Messages file IO |
| `src/TimersLibrary.php` / `TimersFileReader.php` / `TimersFileWriter.php` | Timers file IO |
| `src/StageLibrary.php` / `StageFileReader.php` / `StageFileWriter.php` | Stage file IO |
| `src/WorkspaceLibrary.php` / `WorkspaceFileReader.php` / `WorkspaceFileWriter.php` | Workspace file IO |
| `src/PropLibrary.php` / `PropsFileReader.php` / `PropsFileWriter.php` | Props file IO |
| `src/TestPatternsLibrary.php` / `TestPatternsFileReader.php` / `TestPatternsFileWriter.php` | TestPatterns file IO |
| `src/CalendarLibrary.php` / `CalendarFileReader.php` / `CalendarFileWriter.php` | Calendar file IO |
| `src/KeyMappingsLibrary.php` / `KeyMappingsFileReader.php` / `KeyMappingsFileWriter.php` | KeyMappings file IO |
| `src/CommunicationDevicesLibrary.php` / `CommunicationDevicesFileReader.php` / `CommunicationDevicesFileWriter.php` | CommunicationDevices JSON file IO |
### CLI Tools ### CLI Tools
```bash ```bash
# Parse and display song structure # Songs / playlists / bundles
php bin/parse-song.php path/to/song.pro php bin/parse-song.php path/to/song.pro
# Parse and display playlist structure
php bin/parse-playlist.php path/to/playlist.proplaylist php bin/parse-playlist.php path/to/playlist.proplaylist
# Parse and display the global Macros file # Global library files
php bin/parse-macros.php path/to/Macros php bin/parse-macros.php path/to/Macros
# Parse and display the global Labels file
php bin/parse-labels.php path/to/Labels php bin/parse-labels.php path/to/Labels
php bin/parse-groups.php path/to/Groups
php bin/parse-clear-groups.php path/to/ClearGroups
php bin/parse-ccli.php path/to/CCLI
php bin/parse-messages.php path/to/Messages
php bin/parse-timers.php path/to/Timers
php bin/parse-stage.php path/to/Stage
php bin/parse-workspace.php path/to/Workspace
php bin/parse-props.php path/to/Props
php bin/parse-test-patterns.php path/to/TestPatterns
php bin/parse-calendar.php path/to/Calendar
php bin/parse-key-mappings.php path/to/KeyMappings
php bin/parse-communication-devices.php path/to/CommunicationDevices
# Theme folder
php bin/parse-theme.php path/to/ThemeFolder
``` ```

115
doc/api/calendar.md Normal file
View file

@ -0,0 +1,115 @@
# Calendar Library API
> PHP module for reading and writing the global ProPresenter `Calendar` file
> (raw protobuf, no extension) and preserving scheduled macro events.
## Quick Reference
```php
use ProPresenter\Parser\CalendarFileReader;
$library = CalendarFileReader::read('/path/to/Calendar');
foreach ($library->getEvents() as $event) {
$event->getName();
$event->getStartTimeSeconds();
$event->getActionData(); // raw bytes
}
```
---
## File Layout
The `Calendar` file is the protobuf-serialised
[`CalendarDocument`](../../proto/calendar.proto):
| Field | Type | Description |
|-------|------|-------------|
| `events` | repeated `CalendarDocument.Event` | Scheduled events |
| `mode` | `uint32` | Opaque document mode/source flag |
Each event includes UUID, name, start/end timestamps, opaque `flags`, and two
raw bytes fields: `action_data` (field 8) and `macro_data` (field 9). Those
bytes are intentionally not decoded by this wrapper.
---
## Reading
```php
$library = CalendarFileReader::read('/Users/me/.../Calendar');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## CalendarLibrary
```php
$library->getEvents(); // CalendarEvent[]
$library->count(); // int
$library->getEventByUuid($uuid); // ?CalendarEvent (case-insensitive)
$library->getEventByName($name); // ?CalendarEvent
$library->addEvent($name, $uuid); // CalendarEvent
$library->removeEvent($uuid); // bool
$library->getMode(); // int
$library->setMode(1); // void
$library->getDocument(); // \Rv\Data\CalendarDocument
```
---
## CalendarEvent
```php
$event->getUuid();
$event->setUuid($uuid);
$event->getName();
$event->setName($name);
$event->getStartTime();
$event->setStartTime($timestamp);
$event->getStartTimeSeconds();
$event->setStartTimeSeconds($seconds);
$event->getEndTime();
$event->getEndTimeSeconds();
$event->getFlags();
$event->setFlags($bytes);
$event->getActionData(); // raw protobuf bytes
$event->setActionData($bytes);
$event->getMacroData(); // raw protobuf bytes
$event->setMacroData($bytes);
$event->getProto();
```
---
## CLI Tool
```bash
php bin/parse-calendar.php /path/to/Calendar
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/CalendarLibrary.php` | Document wrapper with UUID / name indexes |
| `src/CalendarEvent.php` | Single event wrapper |
| `src/CalendarFileReader.php` | Reads the `Calendar` file |
| `src/CalendarFileWriter.php` | Writes the `Calendar` file |
| `bin/parse-calendar.php` | CLI summary tool |
| `proto/calendar.proto` | Protobuf schema |
| `generated/Rv/Data/CalendarDocument.php` | Generated document class |
---
## Scope Notes
`action_data` and `macro_data` are raw protobuf byte strings. They are exposed
directly for byte-preserving edits and round trips; semantic decoding belongs in
a future schema-specific module.

104
doc/api/ccli.md Normal file
View file

@ -0,0 +1,104 @@
# CCLI Library API
> PHP module for reading and writing the global ProPresenter `CCLI` file (raw
> protobuf, no extension) that controls copyright display settings.
## Quick Reference
```php
use ProPresenter\Parser\CCLIFileReader;
use ProPresenter\Parser\CCLIFileWriter;
$library = CCLIFileReader::read('/path/to/CCLI');
$library->isCCLIDisplayEnabled(); // bool
$library->getCCLILicense(); // string
$library->getDisplayType(); // int enum value
$library->getTemplate(); // ?\Rv\Data\Template\Slide
$library->setCCLILicense('1234567');
CCLIFileWriter::write($library, '/path/to/CCLI');
```
---
## File Layout
The `CCLI` file is the protobuf-serialised
[`CCLIDocument`](../../proto/ccli.proto):
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter writer metadata |
| `enable_ccli_display` | bool | Whether copyright info is shown |
| `ccli_license` | string | CCLI license number |
| `display_type` | `CCLIDocument.DisplayType` | First, last, first+last, or all slides |
| `template` | `Template.Slide` | Text/template styling for display |
---
## Reading
```php
use ProPresenter\Parser\CCLIFileReader;
$library = CCLIFileReader::read('/Users/me/.../CCLI');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## CCLILibrary
Top-level wrapper around `Rv\Data\CCLIDocument`. This is a single-document
configuration file; `count()` returns `1` when read successfully.
```php
$library->count();
$library->isCCLIDisplayEnabled();
$library->setCCLIDisplayEnabled(true);
$library->getCCLILicense();
$library->setCCLILicense('1234567');
$library->getDisplayType();
$library->setDisplayType(3);
$library->getTemplate();
$library->setTemplate($slideOrNull);
$library->getDocument(); // \Rv\Data\CCLIDocument
```
---
## CLI Tool
```bash
php bin/parse-ccli.php /path/to/CCLI
```
Output:
```
CCLI (1):
[1] enabled=yes :: license=(empty) :: display_type=0 :: template=yes
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/CCLILibrary.php` | Document-level wrapper |
| `src/CCLIFileReader.php` | Reads the `CCLI` file |
| `src/CCLIFileWriter.php` | Writes the `CCLI` file |
| `bin/parse-ccli.php` | CLI tool |
| `proto/ccli.proto` | Protobuf schema |
| `generated/Rv/Data/CCLIDocument.php` | Generated message class |
---
## Scope Notes
The wrapper preserves template data and application metadata by mutating the
generated protobuf in place. It does not inspect or render the slide template.

135
doc/api/clear-groups.md Normal file
View file

@ -0,0 +1,135 @@
# ClearGroups Library API
> PHP module for reading and writing the global ProPresenter `ClearGroups` file
> (raw protobuf, no extension) and exposing each clear group definition.
## Quick Reference
```php
use ProPresenter\Parser\ClearGroupsFileReader;
use ProPresenter\Parser\ClearGroupsFileWriter;
$library = ClearGroupsFileReader::read('/path/to/ClearGroups');
foreach ($library->getGroups() as $group) {
$group->getName(); // "Alles ausblenden"
$group->getUuid(); // "A91C6AFE-..."
$group->getImageType(); // 11
$group->getColorHex(); // "#FFFFFF" | null
}
ClearGroupsFileWriter::write($library, '/path/to/ClearGroups');
```
---
## File Layout
The `ClearGroups` file is the protobuf-serialised
[`ClearGroupsDocument`](../../proto/clearGroups.proto):
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter writer metadata |
| `groups` | repeated `ClearGroupsDocument.ClearGroup` | Clear button definitions |
Each `ClearGroup` carries:
| Field | Type | Description |
|-------|------|-------------|
| `uuid` | `UUID` | Stable identifier |
| `name` | string | Display name |
| `layer_targets` | repeated `Action.ClearType` | Layers cleared by the button |
| `is_hidden_in_preview` | bool | Whether preview UI hides the button |
| `image_data` | bytes | Custom icon payload |
| `image_type` | enum | Built-in icon identifier |
| `is_icon_tinted` | bool | Whether `icon_tint_color` applies |
| `icon_tint_color` | `Color` | RGBA float channels in 0..1 |
| `timeline_targets` | repeated `Action.ContentDestination` | Timeline destinations |
| `clear_presentation_next_slide` | bool | Also clear the queued next slide |
---
## Reading
```php
use ProPresenter\Parser\ClearGroupsFileReader;
$library = ClearGroupsFileReader::read('/Users/me/.../ClearGroups');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## ClearGroupsLibrary
Top-level wrapper around `Rv\Data\ClearGroupsDocument`. Indexes groups by UUID
(case-insensitive) and name.
```php
$library->getGroups(); // ClearGroupDefinition[]
$library->count(); // int
$library->getClearGroupByUuid('A91C...'); // ?ClearGroupDefinition
$library->getClearGroupByName('Alles ...'); // ?ClearGroupDefinition
$library->addClearGroup('Name', 'UUID'); // ClearGroupDefinition
$library->removeClearGroup('UUID'); // bool
$library->getDocument(); // \Rv\Data\ClearGroupsDocument
```
---
## ClearGroupDefinition
```php
$group->getName();
$group->setName('New Name');
$group->getUuid();
$group->setUuid('...');
$group->getLayerTargets();
$group->getImageType();
$group->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
$group->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null
$group->getProto(); // \Rv\Data\ClearGroupsDocument\ClearGroup
```
Color channels are floats in 0..1 as ProPresenter stores them. `getColorHex()`
clamps and rounds each channel to 8 bits before formatting.
---
## CLI Tool
```bash
php bin/parse-clear-groups.php /path/to/ClearGroups
```
Output:
```
ClearGroups (1):
[1] Alles ausblenden :: A91C6AFE-098F-4559-B2CF-D8373C589589 :: image_type=11 :: #FFFFFF
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/ClearGroupsLibrary.php` | Document-level wrapper with UUID/name lookups |
| `src/ClearGroupDefinition.php` | Single clear group wrapper |
| `src/ClearGroupsFileReader.php` | Reads the `ClearGroups` file |
| `src/ClearGroupsFileWriter.php` | Writes the `ClearGroups` file |
| `bin/parse-clear-groups.php` | CLI tool |
| `proto/clearGroups.proto` | Protobuf schema |
| `generated/Rv/Data/ClearGroupsDocument.php` | Generated message class |
---
## Scope Notes
The wrapper preserves unknown / uninterpreted protobuf data by mutating the
generated message in place and serialising it back. It does not execute clear
actions or modify slide content.

View file

@ -0,0 +1,96 @@
# CommunicationDevices Library API
> PHP module for reading and writing the global ProPresenter
> `CommunicationDevices` file. Unlike most config files in this project, this
> file is JSON, not protobuf.
## Quick Reference
```php
use ProPresenter\Parser\CommunicationDevice;
use ProPresenter\Parser\CommunicationDevicesFileReader;
use ProPresenter\Parser\CommunicationDevicesFileWriter;
$library = CommunicationDevicesFileReader::read('/path/to/CommunicationDevices');
$library->addDevice((new CommunicationDevice())->setId('device-1')->setName('Router'));
CommunicationDevicesFileWriter::write($library, '/path/to/CommunicationDevices');
```
---
## File Layout
`CommunicationDevices` is a JSON array. The reference sample is `[]`, so the
wrapper preserves arbitrary object fields while exposing forward-looking common
fields: `id`, `name`, `type`, and `address`.
---
## Reading
```php
$library = CommunicationDevicesFileReader::read('/Users/me/.../CommunicationDevices');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
unreadable files or invalid JSON.
---
## CommunicationDevicesLibrary
```php
CommunicationDevicesLibrary::fromJson($json); // CommunicationDevicesLibrary
$library->toJson(); // string
$library->getDocument(); // raw decoded array
$library->getDevices(); // CommunicationDevice[]
$library->addDevice($device); // CommunicationDevice
$library->removeDevice($id); // bool
$library->count(); // int
```
---
## CommunicationDevice
```php
$device->getId();
$device->setId($id);
$device->getName();
$device->setName($name);
$device->getType();
$device->setType($type);
$device->getAddress();
$device->setAddress($address);
$device->toArray(); // full decoded JSON object
```
---
## CLI Tool
```bash
php bin/parse-communication-devices.php /path/to/CommunicationDevices
```
Empty files print a useful `(none configured)` summary.
---
## Key Files
| File | Purpose |
|------|---------|
| `src/CommunicationDevicesLibrary.php` | JSON-array wrapper |
| `src/CommunicationDevice.php` | Single JSON device value object |
| `src/CommunicationDevicesFileReader.php` | Reads and validates JSON |
| `src/CommunicationDevicesFileWriter.php` | Writes compact JSON |
| `bin/parse-communication-devices.php` | CLI summary tool |
---
## Scope Notes
Because only an empty sample is available, unknown JSON fields are preserved in
each device's backing array. The writer uses compact JSON with unescaped
slashes / Unicode for stable semantic round trips.

172
doc/api/groups.md Normal file
View file

@ -0,0 +1,172 @@
# Groups Library API
> PHP module for the global ProPresenter `Groups` file (raw protobuf, no
> extension). Exposes every named group definition (UUID, name, color,
> hot key) used to organise slides across songs and presentations.
## Quick Reference
```php
use ProPresenter\Parser\GroupsFileReader;
use ProPresenter\Parser\GroupsFileWriter;
$library = GroupsFileReader::read('/path/to/Groups');
foreach ($library->getGroups() as $group) {
$group->getName(); // "Verse 1"
$group->getUuid(); // "1D85C82C-EC82-44D8-8ED0-7742D46242C0"
$group->getColorHex(); // "#0077CC" | null
}
$library->addGroup('Bridge', '...uuid...');
GroupsFileWriter::write($library, '/path/to/Groups');
```
---
## File Layout
The `Groups` file is the protobuf-serialised
[`ProGroupsDocument`](../../proto/groups.proto):
| Field | Type | Description |
|-------|------|-------------|
| `groups` | repeated `Group` | Library group definitions (UUID, name, color, hotKey) |
Each `Group` carries:
| Field | Type | Description |
|-------|------|-------------|
| `uuid` | `UUID` | Stable identifier referenced by song-level cue groups |
| `name` | string | Display name (e.g. "Verse 1") |
| `color` | `Color` (optional) | RGBA float channels |
| `hotKey` | `HotKey` (optional) | Keyboard shortcut binding |
| `application_group_identifier` | `UUID` (optional) | Parent application group |
| `application_group_name` | string (optional) | Parent application group name |
Groups are identified by UUID; names should be unique but the format does
not enforce it.
---
## Reading
```php
use ProPresenter\Parser\GroupsFileReader;
$library = GroupsFileReader::read('/Users/me/.../Groups');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException`
for empty / unreadable files.
---
## Writing
```php
use ProPresenter\Parser\GroupsFileWriter;
GroupsFileWriter::write($library, '/Users/me/.../Groups');
```
The writer serialises the underlying `ProGroupsDocument` back to bytes and
saves them. The unmodified reference sample round-trips byte-for-byte.
---
## GroupLibrary
Top-level wrapper around `Rv\Data\ProGroupsDocument`. Indexes groups by
UUID (case-insensitive) and by name for fast lookup.
```php
$library->getGroups(); // GroupDefinition[]
$library->count(); // int
$library->getGroupByUuid('1D85C82C-...'); // ?GroupDefinition (case-insensitive)
$library->getGroupByName('Verse 1'); // ?GroupDefinition
$library->addGroup('Bridge', '...uuid...'); // GroupDefinition
$library->removeGroup('...uuid...'); // bool
$library->getDocument(); // \Rv\Data\ProGroupsDocument
```
If the same UUID or name appears more than once the first occurrence wins
for lookups; every entry is preserved in `getGroups()` in document order.
---
## GroupDefinition
```php
$group->getUuid(); // "1D85C82C-EC82-44D8-8ED0-7742D46242C0"
$group->setUuid('...'); // self
$group->getName(); // "Verse 1"
$group->setName('Verse 2'); // self
$group->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
$group->getColorHex(); // "#0077CC" | null
$group->setColor(['r'=>1, 'g'=>0, 'b'=>0]); // self
$group->getHotKey(); // ?\Rv\Data\HotKey
$group->getApplicationGroupName(); // string
$group->getApplicationGroupUuid(); // string
$group->getProto(); // \Rv\Data\Group (raw protobuf)
```
The `GroupDefinition` class name is intentionally distinct from the
existing `Group` class which wraps song-level `CueGroup` objects (slide
references, not library definitions).
---
## CLI Tool
```bash
php bin/parse-groups.php /path/to/Groups
```
Output:
```
Groups (29):
[1] Vers :: 4E9D56A2-7E96-4975-97CC-44982257EF8A :: #0077CC
[2] Verse 1 :: 1D85C82C-EC82-44D8-8ED0-7742D46242C0 :: #0077CC
...
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/GroupLibrary.php` | Document-level wrapper with name / UUID lookup |
| `src/GroupDefinition.php` | Single library group (distinct from `Group` / `CueGroup`) |
| `src/GroupsFileReader.php` | Reads the `Groups` file |
| `src/GroupsFileWriter.php` | Writes the `Groups` file |
| `bin/parse-groups.php` | CLI tool |
| `proto/groups.proto` | Protobuf schema |
| `generated/Rv/Data/ProGroupsDocument.php` | Generated message class |
| `generated/Rv/Data/Group.php` | Generated group message class |
---
## Naming Disambiguation
The codebase has two `Group`-shaped classes for two different scopes:
| Class | Scope | Wraps |
|-------|-------|-------|
| `Group` | Song-level slide collection | `Rv\Data\Presentation\CueGroup` |
| `GroupDefinition` | Library-level group definition | `Rv\Data\Group` |
Songs reference library groups by UUID. The two classes co-exist because
ProPresenter's data model has the same name in both places.
---
## Scope Notes
This module covers reading and writing the `Groups` document. Wiring up
hot keys to actions and editing application group hierarchies are out of
scope; reach for `getHotKey()` / `getProto()` to inspect them when needed.

130
doc/api/key-mappings.md Normal file
View file

@ -0,0 +1,130 @@
# KeyMappings Library API
> PHP module for reading and writing the global ProPresenter `KeyMappings` file
> (raw protobuf, no extension) and exposing configured hot-key mappings.
## Quick Reference
```php
use ProPresenter\Parser\KeyMappingsFileReader;
use ProPresenter\Parser\KeyMappingsFileWriter;
$library = KeyMappingsFileReader::read('/path/to/KeyMappings');
foreach ($library->getMappings() as $mapping) {
$mapping->getName(); // string
$mapping->getUuid(); // string
$mapping->getHotKey(); // ?\Rv\Data\HotKey
$mapping->getTarget(); // raw bytes
}
KeyMappingsFileWriter::write($library, '/path/to/KeyMappings');
```
---
## File Layout
The `KeyMappings` file is the protobuf-serialised
[`KeyMappingsDocument`](../../proto/keyMappings.proto):
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter writer metadata |
| `mappings` | repeated `KeyMappingsDocument.Mapping` | Configured key bindings |
Each `Mapping` carries:
| Field | Type | Description |
|-------|------|-------------|
| `uuid` | `UUID` | Optional stable mapping identifier |
| `hot_key` | `HotKey` | Key combo that fires the action |
| `target` | bytes | Raw target reference |
| `name` | string | Optional display name |
---
## Reading
```php
use ProPresenter\Parser\KeyMappingsFileReader;
$library = KeyMappingsFileReader::read('/Users/me/.../KeyMappings');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## KeyMappingsLibrary
Top-level wrapper around `Rv\Data\KeyMappingsDocument`. Indexes mappings by UUID
(case-insensitive) and name.
```php
$library->getMappings();
$library->count();
$library->getMappingByUuid('...');
$library->getMappingByName('Macro trigger');
$library->addMapping('Macro trigger', 'UUID', $targetBytes);
$library->removeMapping('UUID');
$library->getApplicationInfo();
$library->setApplicationInfo($infoOrNull);
$library->getDocument(); // \Rv\Data\KeyMappingsDocument
```
---
## KeyMapping
```php
$mapping->getName();
$mapping->setName('New Name');
$mapping->getUuid();
$mapping->setUuid('...');
$mapping->getHotKey();
$mapping->setHotKey($hotKeyOrNull);
$mapping->getTarget();
$mapping->setTarget($bytes);
$mapping->getProto(); // \Rv\Data\KeyMappingsDocument\Mapping
```
Targets are raw bytes because ProPresenter may encode several internal target
types here. Keeping bytes opaque preserves round-trip safety.
---
## CLI Tool
```bash
php bin/parse-key-mappings.php /path/to/KeyMappings
```
Output for the reference sample:
```
KeyMappings (0):
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/KeyMappingsLibrary.php` | Document-level wrapper with UUID/name lookups |
| `src/KeyMapping.php` | Single mapping wrapper |
| `src/KeyMappingsFileReader.php` | Reads the `KeyMappings` file |
| `src/KeyMappingsFileWriter.php` | Writes the `KeyMappings` file |
| `bin/parse-key-mappings.php` | CLI tool |
| `proto/keyMappings.proto` | Protobuf schema |
| `generated/Rv/Data/KeyMappingsDocument.php` | Generated message class |
---
## Scope Notes
The reference sample contains only `ApplicationInfo` and no mappings. The API
still supports mapping additions/removals so configured user files can be edited
and round-tripped safely.

View file

@ -7,6 +7,7 @@
```php ```php
use ProPresenter\Parser\LabelsFileReader; use ProPresenter\Parser\LabelsFileReader;
use ProPresenter\Parser\LabelsFileWriter;
$library = LabelsFileReader::read('/path/to/Labels'); $library = LabelsFileReader::read('/path/to/Labels');
@ -16,6 +17,14 @@ foreach ($library->getLabels() as $label) {
$label->getColor(); // ['r'=>0.0,'g'=>0.408,'b'=>0.702,'a'=>1.0] | null $label->getColor(); // ['r'=>0.0,'g'=>0.408,'b'=>0.702,'a'=>1.0] | null
$label->getColorHex(); // "#0068B3" | null $label->getColorHex(); // "#0068B3" | null
} }
// Modify and persist
$library->addLabel('NewLabel', ['r' => 1.0, 'g' => 0.5, 'b' => 0.0]);
$beamer = $library->getLabelByName('KeyVisual Beamer');
$beamer?->setColorHex('#FF8800');
$library->removeLabel('Wiederholen');
LabelsFileWriter::write($library, '/path/to/Labels');
``` ```
--- ---
@ -54,6 +63,19 @@ empty / unreadable files.
--- ---
## Writing
```php
use ProPresenter\Parser\LabelsFileWriter;
LabelsFileWriter::write($library, '/Users/me/.../Labels');
```
Serialises the underlying `ProLabelsDocument` to bytes. The unmodified
reference sample round-trips byte-for-byte.
---
## LabelLibrary ## LabelLibrary
Top-level wrapper around `Rv\Data\ProLabelsDocument`. Indexes labels by name Top-level wrapper around `Rv\Data\ProLabelsDocument`. Indexes labels by name
@ -64,6 +86,9 @@ $library->getLabels(); // Label[]
$library->count(); // int $library->count(); // int
$library->getLabelByName('Szene 1'); // ?Label (case-sensitive) $library->getLabelByName('Szene 1'); // ?Label (case-sensitive)
$library->findLabelByName('szene 1'); // ?Label (case-insensitive) $library->findLabelByName('szene 1'); // ?Label (case-insensitive)
$library->addLabel('NewLabel', ['r'=>1, 'g'=>0, 'b'=>0]); // ?Label
$library->removeLabel('OldLabel'); // bool
$library->getDocument(); // \Rv\Data\ProLabelsDocument $library->getDocument(); // \Rv\Data\ProLabelsDocument
``` ```
@ -77,9 +102,13 @@ occurrence wins for both lookup helpers; every entry is preserved in
```php ```php
$label->getName(); // "KeyVisual Beamer" (proto field is `text`) $label->getName(); // "KeyVisual Beamer" (proto field is `text`)
$label->setName('Renamed'); // self
$label->hasColor(); // bool — was a Color message present? $label->hasColor(); // bool — was a Color message present?
$label->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null $label->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
$label->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null $label->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null
$label->setColor(['r'=>1, 'g'=>0, 'b'=>0]); // self
$label->setColor(null); // clears the color (UI falls back to default)
$label->setColorHex('#FF8800'); // accepts #RRGGBB or #RRGGBBAA
$label->getProto(); // \Rv\Data\Action\Label (raw protobuf) $label->getProto(); // \Rv\Data\Action\Label (raw protobuf)
``` ```
@ -118,9 +147,10 @@ Labels (15):
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `src/LabelLibrary.php` | Document-level wrapper with name lookups | | `src/LabelLibrary.php` | Document-level wrapper with name lookups + add / remove helpers |
| `src/Label.php` | Single label wrapper (name, color, hex) | | `src/Label.php` | Single label wrapper (name, color, hex) with setters |
| `src/LabelsFileReader.php` | Reads the `Labels` file | | `src/LabelsFileReader.php` | Reads the `Labels` file |
| `src/LabelsFileWriter.php` | Writes the `Labels` file |
| `bin/parse-labels.php` | CLI tool | | `bin/parse-labels.php` | CLI tool |
| `proto/labels.proto` | Protobuf schema (just imports `Action.Label`) | | `proto/labels.proto` | Protobuf schema (just imports `Action.Label`) |
| `proto/action.proto` | Defines the inner `Action.Label` message | | `proto/action.proto` | Defines the inner `Action.Label` message |
@ -131,7 +161,6 @@ Labels (15):
## Scope Notes ## Scope Notes
This module is read-only by design. Writing the `Labels` file back, editing Editing slide-side label references on `.pro` files (cross-document fan-out)
slide-side label references on `.pro` files, or syncing labels across devices and syncing labels across devices are out of scope; this module only covers
are not implemented here. Add them by mirroring the `ProFileWriter` / the global `Labels` document.
`ProFileGenerator` pattern when needed.

View file

@ -8,18 +8,27 @@
```php ```php
use ProPresenter\Parser\MacrosFileReader; use ProPresenter\Parser\MacrosFileReader;
use ProPresenter\Parser\MacrosFileWriter;
$library = MacrosFileReader::read('/path/to/Macros'); $library = MacrosFileReader::read('/path/to/Macros');
foreach ($library->getMacros() as $macro) { foreach ($library->getMacros() as $macro) {
$macro->getName(); // "Gottesdienst START" $macro->getName(); // "Gottesdienst START"
$macro->getUuid(); // "FA0602E4-EDA2-4457-BB62-68AA17184217" $macro->getUuid(); // "FA0602E4-EDA2-4457-BB62-68AA17184217"
$macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
$macro->getImageType(); // int — see ImageType enum
$macro->getImageData(); // bytes — custom icon (empty for built-ins)
foreach ($library->getCollectionsForMacro($macro) as $collection) { foreach ($library->getCollectionsForMacro($macro) as $collection) {
$collection->getName(); // "Ablauf" $collection->getName();
$collection->getUuid(); // "8D02FC57-83F8-4042-9B90-81C229728426" $collection->getUuid();
} }
} }
// Modify and persist
$library->addMacro('NewMacro', '...uuid...');
$library->getMacroByName('NewMacro')?->setColor(['r'=>0.5, 'g'=>0, 'b'=>1]);
MacrosFileWriter::write($library, '/path/to/Macros');
``` ```
--- ---
@ -53,6 +62,20 @@ empty / unreadable files.
--- ---
## Writing
```php
use ProPresenter\Parser\MacrosFileWriter;
MacrosFileWriter::write($library, '/Users/me/.../Macros');
```
Serialises the underlying `MacrosDocument` to bytes. Round-trip preserves the
overall byte length; field ordering can vary slightly because the protobuf
PHP runtime is not guaranteed to be canonical.
---
## MacroLibrary ## MacroLibrary
Top-level wrapper around `Rv\Data\MacrosDocument`. Indexes macros and Top-level wrapper around `Rv\Data\MacrosDocument`. Indexes macros and
@ -71,6 +94,12 @@ $library->getCollectionByName('Ablauf'); // ?MacroCollection
$library->getMacrosForCollection($collection); // Macro[] in declared order $library->getMacrosForCollection($collection); // Macro[] in declared order
$library->getCollectionsForMacro($macro); // MacroCollection[] (membership) $library->getCollectionsForMacro($macro); // MacroCollection[] (membership)
// Mutators
$library->addMacro('NewMacro', '...uuid...'); // Macro
$library->removeMacro('...uuid...'); // bool
$library->addCollection('NewCollection', '...uuid...'); // MacroCollection
$library->removeCollection('...uuid...'); // bool
$library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf) $library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf)
``` ```
@ -80,11 +109,18 @@ $library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf)
```php ```php
$macro->getUuid(); // "FA0602E4-..." $macro->getUuid(); // "FA0602E4-..."
$macro->setUuid('...'); // self
$macro->getName(); // "Gottesdienst START" $macro->getName(); // "Gottesdienst START"
$macro->setName('...'); // self
$macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null $macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
$macro->setColor(['r'=>0.5,'g'=>0,'b'=>1]); // self (or null to clear)
$macro->getTriggerOnStartup(); // bool $macro->getTriggerOnStartup(); // bool
$macro->setTriggerOnStartup(true); // self
$macro->getActionCount(); // int — number of attached Action entries $macro->getActionCount(); // int — number of attached Action entries
$macro->getImageType(); // int — see Rv\Data\MacrosDocument\Macro\ImageType $macro->getImageType(); // int — see Rv\Data\MacrosDocument\Macro\ImageType
$macro->setImageType(...); // self — pass an ImageType enum value
$macro->getImageData(); // string — custom icon bytes (empty for built-ins)
$macro->setImageData($pngBytes); // self — set a custom icon
$macro->getProto(); // \Rv\Data\MacrosDocument\Macro $macro->getProto(); // \Rv\Data\MacrosDocument\Macro
``` ```
@ -97,8 +133,12 @@ walk `getActions()` directly when needed.
```php ```php
$collection->getUuid(); // "8D02FC57-..." $collection->getUuid(); // "8D02FC57-..."
$collection->setUuid('...'); // self
$collection->getName(); // "Ablauf" $collection->getName(); // "Ablauf"
$collection->setName('...'); // self
$collection->getMacroUuids(); // string[] — referenced macro UUIDs in order $collection->getMacroUuids(); // string[] — referenced macro UUIDs in order
$collection->setMacroUuids(['...']); // self — replace all referenced UUIDs
$collection->addMacroUuid('...'); // self — append a single reference
$collection->getProto(); // \Rv\Data\MacrosDocument\MacroCollection $collection->getProto(); // \Rv\Data\MacrosDocument\MacroCollection
``` ```
@ -132,10 +172,11 @@ Collections (3):
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `src/MacroLibrary.php` | Document-level wrapper with lookup helpers | | `src/MacroLibrary.php` | Document-level wrapper with lookup + add / remove helpers |
| `src/Macro.php` | Single macro wrapper | | `src/Macro.php` | Single macro wrapper with setters |
| `src/MacroCollection.php` | Collection wrapper | | `src/MacroCollection.php` | Collection wrapper with setters |
| `src/MacrosFileReader.php` | Reads the `Macros` file | | `src/MacrosFileReader.php` | Reads the `Macros` file |
| `src/MacrosFileWriter.php` | Writes the `Macros` file |
| `bin/parse-macros.php` | CLI tool | | `bin/parse-macros.php` | CLI tool |
| `proto/macros.proto` | Protobuf schema | | `proto/macros.proto` | Protobuf schema |
| `generated/Rv/Data/MacrosDocument.php` | Generated message classes | | `generated/Rv/Data/MacrosDocument.php` | Generated message classes |
@ -144,8 +185,8 @@ Collections (3):
## Scope Notes ## Scope Notes
This module is read-only by design. Action editing, slide-side macro Action editing (the inner `repeated Action actions` field on each macro) and
references on `.pro` files (see `Slide::getMacroUuid()` / slide-side macro references on `.pro` files (see `Slide::getMacroUuid()` /
`Slide::setMacro()`), and writing the `Macros` file back are not implemented `Slide::setMacro()`) are out of scope. This module covers the global
here. Add them by mirroring the `ProFileWriter` / `ProFileGenerator` pattern `Macros` document only; reach for `getProto()->getActions()` for raw action
when needed. inspection.

110
doc/api/messages.md Normal file
View file

@ -0,0 +1,110 @@
# Messages Library API
> PHP module for reading and writing the global ProPresenter `Messages` file
> (raw protobuf, no extension) and exposing each message definition.
## Quick Reference
```php
use ProPresenter\Parser\MessagesFileReader;
use ProPresenter\Parser\MessagesFileWriter;
$library = MessagesFileReader::read('/path/to/Messages');
foreach ($library->getMessages() as $message) {
$message->getTitle();
$message->getUuid();
$message->getMessageText();
}
$library->addMessage('Lobby Notice', '11111111-1111-1111-1111-111111111111');
MessagesFileWriter::write($library, '/path/to/Messages');
```
---
## File Layout
The `Messages` file is the protobuf-serialised
[`MessageDocument`](../../proto/messages.proto):
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter metadata |
| `messages` | repeated `Message` | Message definitions in document order |
---
## Reading
```php
$library = MessagesFileReader::read('/Users/me/.../Messages');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## MessageLibrary
```php
$library->getMessages(); // Message[]
$library->count(); // int
$library->getMessageByUuid($uuid); // ?Message (case-insensitive)
$library->getMessageByName($title); // ?Message (case-sensitive title)
$library->addMessage($title, $uuid); // Message
$library->removeMessage($uuid); // bool
$library->getApplicationInfo(); // ?\Rv\Data\ApplicationInfo
$library->getDocument(); // \Rv\Data\MessageDocument
```
---
## Message
```php
$message->getUuid();
$message->setUuid($uuid);
$message->getTitle();
$message->setTitle($title);
$message->getTimeToRemove();
$message->setTimeToRemove($seconds);
$message->isVisibleOnNetwork();
$message->setVisibleOnNetwork(true);
$message->getMessageText();
$message->setMessageText($text);
$message->getClearType();
$message->setClearType($enumValue);
$message->getTokens(); // raw repeated Token protos
$message->getProto(); // \Rv\Data\Message
```
---
## CLI Tool
```bash
php bin/parse-messages.php /path/to/Messages
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/MessageLibrary.php` | Document wrapper with title / UUID indexes |
| `src/Message.php` | Single message wrapper |
| `src/MessagesFileReader.php` | Reads the `Messages` file |
| `src/MessagesFileWriter.php` | Writes the `Messages` file |
| `bin/parse-messages.php` | CLI summary tool |
| `generated/Rv/Data/MessageDocument.php` | Generated document class |
---
## Scope Notes
Tokens and token values are preserved as raw generated protobuf objects. The
wrapper exposes them for advanced callers but does not interpret template
rendering semantics.

80
doc/api/props.md Normal file
View file

@ -0,0 +1,80 @@
# Props Library API
> PHP module for reading, modifying, and writing the global ProPresenter `Props` file.
## Quick Reference
```php
use ProPresenter\Parser\PropsFileReader;
use ProPresenter\Parser\PropsFileWriter;
$library = PropsFileReader::read('/path/to/Props');
foreach ($library->getProps() as $prop) {
$prop->getName();
$prop->getUuid();
$prop->isEnabled();
}
PropsFileWriter::write($library, '/path/to/Props');
```
---
## File Layout
The `Props` file is `Rv\Data\PropDocument` from `propDocument.proto`.
Each prop is stored as a `Rv\Data\Cue` in the document's `cues` field.
---
## PropLibrary
```php
$library->getDocument();
$library->getProps();
$library->getPropByUuid('1FB2...'); // case-insensitive
$library->getPropByName('Props #1');
$library->addProp($prop);
$library->removeProp($uuid);
$library->count();
$library->getApplicationInfo();
```
---
## Prop
```php
$prop->getName();
$prop->setName('Lower Third');
$prop->getUuid();
$prop->setUuid('...');
$prop->isEnabled();
$prop->setEnabled(true);
$prop->getCompletionTime();
$prop->getActions();
$prop->getProto();
```
Use `getProto()` for full Cue/action access.
---
## CLI Tool
```bash
php bin/parse-props.php /path/to/Props
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/PropsFileReader.php` | Reads the `Props` file |
| `src/PropsFileWriter.php` | Writes the `Props` file |
| `src/PropLibrary.php` | Document wrapper and indexes |
| `src/Prop.php` | Single Cue/prop wrapper |
| `bin/parse-props.php` | CLI summary tool |

83
doc/api/stage.md Normal file
View file

@ -0,0 +1,83 @@
# Stage Library API
> PHP module for reading, modifying, and writing the global ProPresenter `Stage` file.
## Quick Reference
```php
use ProPresenter\Parser\StageFileReader;
use ProPresenter\Parser\StageFileWriter;
$library = StageFileReader::read('/path/to/Stage');
foreach ($library->getLayouts() as $layout) {
$layout->getName();
$layout->getUuid();
$layout->getSlide(); // ?\Rv\Data\Slide
}
StageFileWriter::write($library, '/path/to/Stage');
```
---
## File Layout
The `Stage` file is the protobuf-serialised `Rv\Data\Stage\Document` from
`stage.proto`.
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter metadata |
| `layouts` | repeated `Stage.Layout` | Stage display layouts |
---
## StageLibrary
```php
$library->getDocument();
$library->getLayouts();
$library->getLayoutByUuid('0455...'); // case-insensitive
$library->getLayoutByName('Default StageDisplay');
$library->addLayout($layout);
$library->removeLayout($uuid);
$library->count();
$library->getApplicationInfo();
```
---
## StageLayout
```php
$layout->getName();
$layout->setName('New name');
$layout->getUuid();
$layout->setUuid('...');
$layout->getSlide();
$layout->getProto();
```
The slide is exposed as the raw `Rv\Data\Slide` protobuf because stage layouts
can contain complex arrangements.
---
## CLI Tool
```bash
php bin/parse-stage.php /path/to/Stage
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/StageFileReader.php` | Reads the `Stage` file |
| `src/StageFileWriter.php` | Writes the `Stage` file |
| `src/StageLibrary.php` | Document wrapper and indexes |
| `src/StageLayout.php` | Single layout wrapper |
| `bin/parse-stage.php` | CLI summary tool |

106
doc/api/test-patterns.md Normal file
View file

@ -0,0 +1,106 @@
# TestPatterns Library API
> PHP module for reading and writing the global ProPresenter `TestPatterns` file
> (raw protobuf, no extension), including selected test-pattern state and saved
> pattern definitions.
## Quick Reference
```php
use ProPresenter\Parser\TestPatternsFileReader;
use ProPresenter\Parser\TestPatternsFileWriter;
$library = TestPatternsFileReader::read('/path/to/TestPatterns');
$library->getDisplayLocation(); // 3
$library->getSpecificScreenUuid(); // "BCDE1115-..."
$library->getPatterns(); // TestPatternData[]
TestPatternsFileWriter::write($library, '/path/to/TestPatterns');
```
---
## File Layout
The `TestPatterns` file is the protobuf-serialised
[`TestPatternDocument`](../../proto/testPattern.proto):
| Field | Type | Description |
|-------|------|-------------|
| `state` | `TestPatternDocument.TestPatternStateData` | Current test-pattern display state |
| `patterns` | repeated `TestPatternDocument.TestPatternData` | Saved pattern definitions |
`TestPatternStateData` includes selected pattern UUID/name, display location,
specific screen UUID, identify-screen flag, logo type, and optional user logo.
---
## Reading
```php
use ProPresenter\Parser\TestPatternsFileReader;
$library = TestPatternsFileReader::read('/Users/me/.../TestPatterns');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## TestPatternsLibrary
Top-level wrapper around `Rv\Data\TestPatternDocument`. Indexes saved pattern
definitions by UUID (case-insensitive) and localization key.
```php
$library->getState();
$library->setState($stateOrNull);
$library->getSelectedPatternUuid();
$library->getSelectedPatternNameLocalizationKey();
$library->getDisplayLocation();
$library->getSpecificScreenUuid();
$library->getPatterns();
$library->count();
$library->getPatternByUuid('...');
$library->getPatternByName('Test Pattern');
$library->addPattern('Test Pattern', 'UUID');
$library->removePattern('UUID');
$library->getDocument(); // \Rv\Data\TestPatternDocument
```
---
## CLI Tool
```bash
php bin/parse-test-patterns.php /path/to/TestPatterns
```
Output:
```
TestPatterns (0):
State: selected=(none) :: name=(none) :: display_location=3 :: screen=BCDE1115-AD40-4BA4-A33A-BFFE3E87223B
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/TestPatternsLibrary.php` | Document-level wrapper with state accessors |
| `src/TestPatternsFileReader.php` | Reads the `TestPatterns` file |
| `src/TestPatternsFileWriter.php` | Writes the `TestPatterns` file |
| `bin/parse-test-patterns.php` | CLI tool |
| `proto/testPattern.proto` | Protobuf schema |
| `generated/Rv/Data/TestPatternDocument.php` | Generated message class |
---
## Scope Notes
The wrapper exposes `TestPatternData` and `TestPatternStateData` protobufs
directly. It does not render test patterns or interpret nested property oneofs.

116
doc/api/theme.md Normal file
View file

@ -0,0 +1,116 @@
# Theme Bundle API
> PHP module for reading, modifying, and writing folder-based ProPresenter themes.
## Quick Reference
```php
use ProPresenter\Parser\ThemeFileReader;
use ProPresenter\Parser\ThemeFileWriter;
$theme = ThemeFileReader::read('/path/to/theme-folder');
foreach ($theme->getSlides() as $slide) {
$slide->getName(); // KeyVisual, Liedtext, ...
$slide->getBaseSlide(); // ?\Rv\Data\Slide
}
foreach ($theme->getAssets() as $asset) {
$asset->getName();
$asset->getSize();
$asset->getMimeType();
}
ThemeFileWriter::write($theme, '/path/to/output-folder');
```
---
## Folder Layout
A theme is a directory, not a single protobuf file:
```text
SampleTheme/
├── Theme # Rv\Data\Template\Document protobuf
└── Assets/
├── BACKGROUND.jpg
├── BAUCHBIND_STREAM.jpg
└── KEY_VISUAL.jpg
```
The `Theme` file is a `Rv\Data\Template\Document` from `template.proto`.
Its slides are named theme layouts.
---
## ThemeBundle
```php
$theme->getDocument();
$theme->getSlides();
$theme->getSlideByName('KeyVisual');
$theme->addSlide($slide);
$theme->removeSlide('KeyVisual');
$theme->getAssets();
$theme->getAssetByName('BACKGROUND.jpg');
$theme->addAsset('NEW.jpg', $bytes);
$theme->removeAsset('NEW.jpg');
$theme->count();
$theme->getAssetCount();
```
---
## ThemeSlide
```php
$slide->getName();
$slide->setName('Liedtext');
$slide->getBaseSlide();
$slide->getProto();
```
---
## ThemeAsset
```php
$asset->getName();
$asset->getBytes();
$asset->getSize();
$asset->getMimeType(); // image/jpeg, image/png, ...
```
MIME type detection is extension-based and best-effort.
---
## Writing Themes
`ThemeFileWriter::write()` creates the target folder if needed, writes the
serialized `Theme` protobuf, creates `Assets/`, writes every `ThemeAsset`, and
removes stale files from `Assets/` that are not present in the bundle.
---
## CLI Tool
```bash
php bin/parse-theme.php /path/to/theme-folder
```
The CLI prints slide names plus asset filenames, sizes, and MIME types.
---
## Key Files
| File | Purpose |
|------|---------|
| `src/ThemeBundle.php` | Top-level theme wrapper |
| `src/ThemeFileReader.php` | Reads a theme folder |
| `src/ThemeFileWriter.php` | Writes a theme folder and cleans stale assets |
| `src/ThemeSlide.php` | Single template slide wrapper |
| `src/ThemeAsset.php` | Single asset value object |
| `bin/parse-theme.php` | CLI summary tool |

106
doc/api/timers.md Normal file
View file

@ -0,0 +1,106 @@
# Timers Library API
> PHP module for reading and writing the global ProPresenter `Timers` file
> (raw protobuf, no extension), including the top-level clock format.
## Quick Reference
```php
use ProPresenter\Parser\TimersFileReader;
$library = TimersFileReader::read('/path/to/Timers');
$library->getClockFormat(); // "HH:mm"
foreach ($library->getTimers() as $timer) {
$timer->getName();
$timer->isCountdown();
$timer->getDurationSeconds();
}
```
---
## File Layout
The `Timers` file is the protobuf-serialised
[`TimersDocument`](../../proto/timers.proto):
| Field | Type | Description |
|-------|------|-------------|
| `application_info` | `ApplicationInfo` | ProPresenter metadata |
| `clock` | `Clock` | Global clock display settings |
| `timers` | repeated `Timer` | Timer definitions |
---
## Reading
```php
$library = TimersFileReader::read('/Users/me/.../Timers');
```
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
empty / unreadable files.
---
## TimersLibrary
```php
$library->getTimers(); // Timer[]
$library->count(); // int
$library->getTimerByUuid($uuid); // ?Timer (case-insensitive)
$library->getTimerByName($name); // ?Timer
$library->addTimer($name, $uuid); // Timer
$library->removeTimer($uuid); // bool
$library->getClockFormat(); // string
$library->setClockFormat('HH:mm'); // void
$library->getApplicationInfo(); // ?\Rv\Data\ApplicationInfo
$library->getDocument(); // \Rv\Data\TimersDocument
```
---
## Timer
```php
$timer->getUuid();
$timer->setUuid($uuid);
$timer->getName();
$timer->setName($name);
$timer->getConfiguration(); // ?\Rv\Data\Timer\Configuration
$timer->isCountdown();
$timer->isCountdownToTime();
$timer->isElapsedTime();
$timer->getDurationSeconds(); // ?int for countdown timers
$timer->getProto(); // \Rv\Data\Timer
```
---
## CLI Tool
```bash
php bin/parse-timers.php /path/to/Timers
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/TimersLibrary.php` | Document wrapper with UUID / name indexes |
| `src/Timer.php` | Single timer wrapper |
| `src/TimersFileReader.php` | Reads the `Timers` file |
| `src/TimersFileWriter.php` | Writes the `Timers` file |
| `bin/parse-timers.php` | CLI summary tool |
| `generated/Rv/Data/TimersDocument.php` | Generated document class |
---
## Scope Notes
Timer configuration is exposed as the generated protobuf sub-message. Helper
methods cover the oneof timer type and countdown duration without hiding raw
access for callers that need advanced fields.

83
doc/api/workspace.md Normal file
View file

@ -0,0 +1,83 @@
# Workspace Library API
> PHP module for reading, modifying, and writing the ProPresenter `Workspace` file.
## Quick Reference
```php
use ProPresenter\Parser\WorkspaceFileReader;
use ProPresenter\Parser\WorkspaceFileWriter;
$library = WorkspaceFileReader::read('/path/to/Workspace');
foreach ($library->getScreens() as $screen) {
$screen->getName();
$screen->getUuid();
$screen->getScreenType();
}
WorkspaceFileWriter::write($library, '/path/to/Workspace');
```
---
## File Layout
The `Workspace` file is `Rv\Data\ProPresenterWorkspace` from `proworkspace.proto`.
Its `pro_screens` entries are `Rv\Data\ProPresenterScreen` messages from
`proscreen.proto`.
---
## WorkspaceLibrary
```php
$library->getDocument();
$library->getScreens();
$library->getScreenByName('StageDisplay');
$library->getScreenByUuid('C86D...'); // case-insensitive
$library->addScreen($screen);
$library->removeScreen($uuid);
$library->count();
$library->getAudienceLooks();
$library->getMasks();
$library->getVideoInputs();
$library->getSelectedLibraryName();
$library->setSelectedLibraryName('Library');
```
---
## Screen
```php
$screen->getName();
$screen->setName('New name');
$screen->getUuid();
$screen->setUuid('...');
$screen->getScreenType();
$screen->getIndex();
$screen->getProto();
```
Use `getProto()` for detailed arrangement, background, and screen geometry data.
---
## CLI Tool
```bash
php bin/parse-workspace.php /path/to/Workspace
```
---
## Key Files
| File | Purpose |
|------|---------|
| `src/WorkspaceFileReader.php` | Reads the `Workspace` file |
| `src/WorkspaceFileWriter.php` | Writes the `Workspace` file |
| `src/WorkspaceLibrary.php` | Document wrapper and indexes |
| `src/Screen.php` | Single screen wrapper |
| `bin/parse-workspace.php` | CLI summary tool |

View file

@ -13,30 +13,34 @@
| ZIP | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | ZIP | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| JSON | [api/communication-devices.md](api/communication-devices.md) |
## Song Structure ## Song Structure
| Keyword | Document | | Keyword | Document |
|---------|----------| |---------|----------|
| song | [api/song.md](api/song.md) | | song | [api/song.md](api/song.md) |
| group | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 | | group | [api/song.md](api/song.md), [api/groups.md](api/groups.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 |
| slide | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | slide | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| arrangement | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 6 | | arrangement | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 6 |
| translation | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 7 | | translation | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 7 |
| verse | [api/song.md](api/song.md) | | verse | [api/song.md](api/song.md) |
| chorus | [api/song.md](api/song.md) | | chorus | [api/song.md](api/song.md) |
| lyrics | [api/song.md](api/song.md) | | lyrics | [api/song.md](api/song.md) |
| CCLI | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | | CCLI | [api/ccli.md](api/ccli.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 |
## Bundle Structure ## Bundle Structure
| Keyword | Document | | Keyword | Document |
|---------|----------| |---------|----------|
| bundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | bundle | [api/bundle.md](api/bundle.md), [api/theme.md](api/theme.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| probundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | probundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
| pro6x | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 1 | | pro6x | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 1 |
| LocalRelativePath | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | | LocalRelativePath | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
| absolute path | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 2 | | absolute path | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 2 |
| theme | [api/theme.md](api/theme.md) |
| theme folder | [api/theme.md](api/theme.md) |
| assets | [api/theme.md](api/theme.md), [api/bundle.md](api/bundle.md) |
## Playlist Structure ## Playlist Structure
@ -70,25 +74,86 @@
| Macros file | [api/macros.md](api/macros.md) | | Macros file | [api/macros.md](api/macros.md) |
| MacroCollection | [api/macros.md](api/macros.md) | | MacroCollection | [api/macros.md](api/macros.md) |
| MacroLibrary | [api/macros.md](api/macros.md) | | MacroLibrary | [api/macros.md](api/macros.md) |
| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | | media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [api/theme.md](api/theme.md) |
| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | | image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3, [api/theme.md](api/theme.md) |
| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 | | video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 |
| cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [api/props.md](api/props.md) |
| label | [api/labels.md](api/labels.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | label | [api/labels.md](api/labels.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| Labels file | [api/labels.md](api/labels.md) | | Labels file | [api/labels.md](api/labels.md) |
| LabelLibrary | [api/labels.md](api/labels.md) | | LabelLibrary | [api/labels.md](api/labels.md) |
| LabelsFileReader | [api/labels.md](api/labels.md) | | LabelsFileReader | [api/labels.md](api/labels.md) |
| color | [api/labels.md](api/labels.md), [api/macros.md](api/macros.md) | | color | [api/labels.md](api/labels.md), [api/macros.md](api/macros.md), [api/groups.md](api/groups.md), [api/clear-groups.md](api/clear-groups.md) |
## Global library files
| Keyword | Document |
|---------|----------|
| Groups file | [api/groups.md](api/groups.md) |
| GroupLibrary | [api/groups.md](api/groups.md) |
| GroupDefinition | [api/groups.md](api/groups.md) |
| ClearGroups file | [api/clear-groups.md](api/clear-groups.md) |
| ClearGroup | [api/clear-groups.md](api/clear-groups.md) |
| ClearGroupDefinition | [api/clear-groups.md](api/clear-groups.md) |
| CCLI file | [api/ccli.md](api/ccli.md) |
| CCLILibrary | [api/ccli.md](api/ccli.md) |
| copyright | [api/ccli.md](api/ccli.md) |
| license | [api/ccli.md](api/ccli.md) |
| Messages file | [api/messages.md](api/messages.md) |
| MessageLibrary | [api/messages.md](api/messages.md) |
| Message | [api/messages.md](api/messages.md) |
| token | [api/messages.md](api/messages.md) |
| Timers file | [api/timers.md](api/timers.md) |
| TimersLibrary | [api/timers.md](api/timers.md) |
| Timer | [api/timers.md](api/timers.md) |
| Clock | [api/timers.md](api/timers.md) |
| countdown | [api/timers.md](api/timers.md) |
| Stage file | [api/stage.md](api/stage.md) |
| StageLibrary | [api/stage.md](api/stage.md) |
| StageLayout | [api/stage.md](api/stage.md) |
| stage display | [api/stage.md](api/stage.md) |
| Workspace file | [api/workspace.md](api/workspace.md) |
| WorkspaceLibrary | [api/workspace.md](api/workspace.md) |
| Screen | [api/workspace.md](api/workspace.md) |
| audience look | [api/workspace.md](api/workspace.md) |
| mask | [api/workspace.md](api/workspace.md) |
| video input | [api/workspace.md](api/workspace.md) |
| Props file | [api/props.md](api/props.md) |
| PropLibrary | [api/props.md](api/props.md) |
| Prop | [api/props.md](api/props.md) |
| TestPatterns file | [api/test-patterns.md](api/test-patterns.md) |
| TestPatternsLibrary | [api/test-patterns.md](api/test-patterns.md) |
| test pattern | [api/test-patterns.md](api/test-patterns.md) |
| Calendar file | [api/calendar.md](api/calendar.md) |
| CalendarLibrary | [api/calendar.md](api/calendar.md) |
| CalendarEvent | [api/calendar.md](api/calendar.md) |
| schedule | [api/calendar.md](api/calendar.md) |
| KeyMappings file | [api/key-mappings.md](api/key-mappings.md) |
| KeyMappingsLibrary | [api/key-mappings.md](api/key-mappings.md) |
| KeyMapping | [api/key-mappings.md](api/key-mappings.md) |
| hot key | [api/key-mappings.md](api/key-mappings.md), [api/groups.md](api/groups.md) |
| CommunicationDevices file | [api/communication-devices.md](api/communication-devices.md) |
| CommunicationDevicesLibrary | [api/communication-devices.md](api/communication-devices.md) |
| CommunicationDevice | [api/communication-devices.md](api/communication-devices.md) |
| MIDI | [api/communication-devices.md](api/communication-devices.md) |
| OSC | [api/communication-devices.md](api/communication-devices.md) |
| Theme | [api/theme.md](api/theme.md) |
| ThemeBundle | [api/theme.md](api/theme.md) |
| ThemeSlide | [api/theme.md](api/theme.md) |
| ThemeAsset | [api/theme.md](api/theme.md) |
## PHP API ## PHP API
| Keyword | Document | | Keyword | Document |
|---------|----------| |---------|----------|
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | | read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) |
| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | | write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md), [api/theme.md](api/theme.md) |
| generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | | generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | | parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) |
| MacrosFileReader | [api/macros.md](api/macros.md) | | MacrosFileReader | [api/macros.md](api/macros.md) |
| MacrosFileWriter | [api/macros.md](api/macros.md) |
| LabelsFileWriter | [api/labels.md](api/labels.md) |
| GroupsFileReader | [api/groups.md](api/groups.md) |
| GroupsFileWriter | [api/groups.md](api/groups.md) |
| ProFileReader | [api/song.md](api/song.md) | | ProFileReader | [api/song.md](api/song.md) |
| ProFileWriter | [api/song.md](api/song.md) | | ProFileWriter | [api/song.md](api/song.md) |
| ProFileGenerator | [api/song.md](api/song.md) | | ProFileGenerator | [api/song.md](api/song.md) |
@ -98,9 +163,11 @@
| ProBundleReader | [api/bundle.md](api/bundle.md) | | ProBundleReader | [api/bundle.md](api/bundle.md) |
| ProBundleWriter | [api/bundle.md](api/bundle.md) | | ProBundleWriter | [api/bundle.md](api/bundle.md) |
| PresentationBundle | [api/bundle.md](api/bundle.md) | | PresentationBundle | [api/bundle.md](api/bundle.md) |
| ThemeFileReader | [api/theme.md](api/theme.md) |
| ThemeFileWriter | [api/theme.md](api/theme.md) |
| Song | [api/song.md](api/song.md) | | Song | [api/song.md](api/song.md) |
| PlaylistArchive | [api/playlist.md](api/playlist.md) | | PlaylistArchive | [api/playlist.md](api/playlist.md) |
| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | | CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) |
| command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | | command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) |
## Protobuf ## Protobuf
@ -109,13 +176,25 @@
|---------|----------| |---------|----------|
| Presentation | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | | Presentation | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 |
| CueGroup | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 | | CueGroup | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 |
| Cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | Cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [api/props.md](api/props.md) |
| Action | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | Action | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
| Playlist | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 3 | | Playlist | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 3 |
| PlaylistItem | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 5 | | PlaylistItem | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 5 |
| UUID | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | UUID | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
| field number | [formats/pp_song_spec.md](formats/pp_song_spec.md) Appendix | | field number | [formats/pp_song_spec.md](formats/pp_song_spec.md) Appendix |
| proto | [formats/pp_song_spec.md](formats/pp_song_spec.md) | | proto | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
| Template.Document | [api/theme.md](api/theme.md) |
| ProPresenterWorkspace | [api/workspace.md](api/workspace.md) |
| ProGroupsDocument | [api/groups.md](api/groups.md) |
| ClearGroupsDocument | [api/clear-groups.md](api/clear-groups.md) |
| MessageDocument | [api/messages.md](api/messages.md) |
| TimersDocument | [api/timers.md](api/timers.md) |
| Stage.Document | [api/stage.md](api/stage.md) |
| PropDocument | [api/props.md](api/props.md) |
| TestPatternDocument | [api/test-patterns.md](api/test-patterns.md) |
| CalendarDocument | [api/calendar.md](api/calendar.md) |
| KeyMappingsDocument | [api/key-mappings.md](api/key-mappings.md) |
| CCLIDocument | [api/ccli.md](api/ccli.md) |
## Troubleshooting ## Troubleshooting

BIN
doc/reference_samples/CCLI Normal file

Binary file not shown.

View file

@ -0,0 +1,49 @@
ƒ
&
$3E749EF4-0663-4F0F-AACA-BD801B6D8ACD
Doors Open"ŒÚæ¹*2 ü²‹ºÙ€¡JBzf"v
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ¸
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450HÂv
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer
ù
&
$794A1711-84DB-42F6-8012-B43B1F395096
Godi Start"ˆâæ¹*B{f"w
u
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJº
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:020HÂw
u
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer
ó
&
$81394F5E-E776-458B-BB06-5C87B3C46D94
Doors Open"Œ¬†ºBzf"v
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ¸
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450HÂv
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer

Binary file not shown.

View file

@ -0,0 +1 @@
[]

Binary file not shown.

View file

@ -0,0 +1,2 @@
 "" 117899279

Binary file not shown.

BIN
doc/reference_samples/Props Normal file

Binary file not shown.

BIN
doc/reference_samples/Stage Normal file

Binary file not shown.

View file

@ -0,0 +1,3 @@
*"&
$BCDE1115-AD40-4BA4-A33A-BFFE3E87223B

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,49 @@
ƒ
&
$3E749EF4-0663-4F0F-AACA-BD801B6D8ACD
Doors Open"ŒÚæ¹*2 ü²‹ºÙ€¡JBzf"v
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ¸
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450HÂv
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer
ù
&
$794A1711-84DB-42F6-8012-B43B1F395096
Godi Start"ˆâæ¹*B{f"w
u
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJº
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:020HÂw
u
&
$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer
ó
&
$81394F5E-E776-458B-BB06-5C87B3C46D94
Doors Open"Œ¬†ºBzf"v
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ¸
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450HÂv
t
&
$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457
&
$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer

Binary file not shown.

View file

@ -0,0 +1 @@
[]

Binary file not shown.

View file

@ -0,0 +1,2 @@
 "" 117899279

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,3 @@
*"&
$BCDE1115-AD40-4BA4-A33A-BFFE3E87223B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

View file

@ -0,0 +1,27 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: calendar.proto
namespace GPBMetadata;
class Calendar
{
public static $is_initialized = false;
public static function initOnce() {
$pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
if (static::$is_initialized == true) {
return;
}
\GPBMetadata\Rvtimestamp::initOnce();
\GPBMetadata\Uuid::initOnce();
$pool->internalAddGeneratedFile(
"\x0A\xF2\x02\x0A\x0Ecalendar.proto\x12\x07rv.data\x1A\x0Auuid.proto\"\x8C\x02\x0A\x10CalendarDocument\x12/\x0A\x06events\x18\x01 \x03(\x0B2\x1F.rv.data.CalendarDocument.Event\x12\x0C\x0A\x04mode\x18\x02 \x01(\x0D\x1A\xB8\x01\x0A\x05Event\x12\x1B\x0A\x04uuid\x18\x01 \x01(\x0B2\x0D.rv.data.UUID\x12\x0C\x0A\x04name\x18\x02 \x01(\x09\x12&\x0A\x0Astart_time\x18\x04 \x01(\x0B2\x12.rv.data.Timestamp\x12\x0D\x0A\x05flags\x18\x05 \x01(\x0C\x12\$\x0A\x08end_time\x18\x06 \x01(\x0B2\x12.rv.data.Timestamp\x12\x13\x0A\x0Baction_data\x18\x08 \x01(\x0C\x12\x12\x0A\x0Amacro_data\x18\x09 \x01(\x0CB4\xF8\x01\x01\xAA\x02\$Pro.SerializationInterop.RVProtoData\xBA\x02\x07RVData_b\x06proto3"
, true);
static::$is_initialized = true;
}
}

View file

@ -0,0 +1,28 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: keyMappings.proto
namespace GPBMetadata;
class KeyMappings
{
public static $is_initialized = false;
public static function initOnce() {
$pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
if (static::$is_initialized == true) {
return;
}
\GPBMetadata\ApplicationInfo::initOnce();
\GPBMetadata\HotKey::initOnce();
\GPBMetadata\Uuid::initOnce();
$pool->internalAddGeneratedFile(
"\x0A\xE0\x02\x0A\x11keyMappings.proto\x12\x07rv.data\x1A\x0ChotKey.proto\x1A\x0Auuid.proto\"\xE9\x01\x0A\x13KeyMappingsDocument\x122\x0A\x10application_info\x18\x01 \x01(\x0B2\x18.rv.data.ApplicationInfo\x126\x0A\x08mappings\x18\x02 \x03(\x0B2\$.rv.data.KeyMappingsDocument.Mapping\x1Af\x0A\x07Mapping\x12\x1B\x0A\x04uuid\x18\x01 \x01(\x0B2\x0D.rv.data.UUID\x12 \x0A\x07hot_key\x18\x02 \x01(\x0B2\x0F.rv.data.HotKey\x12\x0E\x0A\x06target\x18\x03 \x01(\x0C\x12\x0C\x0A\x04name\x18\x04 \x01(\x09B4\xF8\x01\x01\xAA\x02\$Pro.SerializationInterop.RVProtoData\xBA\x02\x07RVData_b\x06proto3"
, true);
static::$is_initialized = true;
}
}

View file

@ -0,0 +1,107 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: calendar.proto
namespace Rv\Data;
use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;
/**
* CalendarDocument is the global ProPresenter `Calendar` file. It holds
* scheduled events that fire macros at given times. The exact semantics of
* the inner sub-messages 8 and 9 (action and embedded macro reference) are
* preserved as raw bytes so the document round-trips byte-for-byte; clients
* that need to inspect them can decode them with `protoc --decode_raw` or
* reach for the raw protobuf message.
*
* Generated from protobuf message <code>rv.data.CalendarDocument</code>
*/
class CalendarDocument extends \Google\Protobuf\Internal\Message
{
/**
* Events scheduled in the calendar, in the order ProPresenter wrote them.
*
* Generated from protobuf field <code>repeated .rv.data.CalendarDocument.Event events = 1;</code>
*/
private $events;
/**
* Source / mode flag observed in samples (value `1`). Treated as opaque.
*
* Generated from protobuf field <code>uint32 mode = 2;</code>
*/
protected $mode = 0;
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type array<\Rv\Data\CalendarDocument\Event>|\Google\Protobuf\Internal\RepeatedField $events
* Events scheduled in the calendar, in the order ProPresenter wrote them.
* @type int $mode
* Source / mode flag observed in samples (value `1`). Treated as opaque.
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\Calendar::initOnce();
parent::__construct($data);
}
/**
* Events scheduled in the calendar, in the order ProPresenter wrote them.
*
* Generated from protobuf field <code>repeated .rv.data.CalendarDocument.Event events = 1;</code>
* @return \Google\Protobuf\Internal\RepeatedField
*/
public function getEvents()
{
return $this->events;
}
/**
* Events scheduled in the calendar, in the order ProPresenter wrote them.
*
* Generated from protobuf field <code>repeated .rv.data.CalendarDocument.Event events = 1;</code>
* @param array<\Rv\Data\CalendarDocument\Event>|\Google\Protobuf\Internal\RepeatedField $var
* @return $this
*/
public function setEvents($var)
{
$arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Rv\Data\CalendarDocument\Event::class);
$this->events = $arr;
return $this;
}
/**
* Source / mode flag observed in samples (value `1`). Treated as opaque.
*
* Generated from protobuf field <code>uint32 mode = 2;</code>
* @return int
*/
public function getMode()
{
return $this->mode;
}
/**
* Source / mode flag observed in samples (value `1`). Treated as opaque.
*
* Generated from protobuf field <code>uint32 mode = 2;</code>
* @param int $var
* @return $this
*/
public function setMode($var)
{
GPBUtil::checkUint32($var);
$this->mode = $var;
return $this;
}
}

View file

@ -0,0 +1,316 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: calendar.proto
namespace Rv\Data\CalendarDocument;
use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;
/**
* Generated from protobuf message <code>rv.data.CalendarDocument.Event</code>
*/
class Event extends \Google\Protobuf\Internal\Message
{
/**
* Stable identity of this calendar event.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
*/
protected $uuid = null;
/**
* Display name (e.g. "Doors Open").
*
* Generated from protobuf field <code>string name = 2;</code>
*/
protected $name = '';
/**
* When the event starts. Stored as Timestamp seconds (and optional nanos).
*
* Generated from protobuf field <code>.rv.data.Timestamp start_time = 4;</code>
*/
protected $start_time = null;
/**
* Opaque flags blob observed in samples. Often a single byte (e.g. 0x01)
* that ProPresenter uses for recurrence or visibility state.
*
* Generated from protobuf field <code>bytes flags = 5;</code>
*/
protected $flags = '';
/**
* When the event ends. Optional. Same format as `start_time`.
*
* Generated from protobuf field <code>.rv.data.Timestamp end_time = 6;</code>
*/
protected $end_time = null;
/**
* Action that runs when the event fires. Encoded as raw protobuf bytes
* (a `rv.data.Action`-shaped message) so the schema can evolve without
* breaking existing clients.
*
* Generated from protobuf field <code>bytes action_data = 8;</code>
*/
protected $action_data = '';
/**
* Embedded copy of the macro definition the event triggers. Stored as
* raw protobuf bytes (shape compatible with `MacrosDocument.Macro`).
*
* Generated from protobuf field <code>bytes macro_data = 9;</code>
*/
protected $macro_data = '';
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type \Rv\Data\UUID $uuid
* Stable identity of this calendar event.
* @type string $name
* Display name (e.g. "Doors Open").
* @type \Rv\Data\Timestamp $start_time
* When the event starts. Stored as Timestamp seconds (and optional nanos).
* @type string $flags
* Opaque flags blob observed in samples. Often a single byte (e.g. 0x01)
* that ProPresenter uses for recurrence or visibility state.
* @type \Rv\Data\Timestamp $end_time
* When the event ends. Optional. Same format as `start_time`.
* @type string $action_data
* Action that runs when the event fires. Encoded as raw protobuf bytes
* (a `rv.data.Action`-shaped message) so the schema can evolve without
* breaking existing clients.
* @type string $macro_data
* Embedded copy of the macro definition the event triggers. Stored as
* raw protobuf bytes (shape compatible with `MacrosDocument.Macro`).
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\Calendar::initOnce();
parent::__construct($data);
}
/**
* Stable identity of this calendar event.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
* @return \Rv\Data\UUID|null
*/
public function getUuid()
{
return $this->uuid;
}
public function hasUuid()
{
return isset($this->uuid);
}
public function clearUuid()
{
unset($this->uuid);
}
/**
* Stable identity of this calendar event.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
* @param \Rv\Data\UUID $var
* @return $this
*/
public function setUuid($var)
{
GPBUtil::checkMessage($var, \Rv\Data\UUID::class);
$this->uuid = $var;
return $this;
}
/**
* Display name (e.g. "Doors Open").
*
* Generated from protobuf field <code>string name = 2;</code>
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Display name (e.g. "Doors Open").
*
* Generated from protobuf field <code>string name = 2;</code>
* @param string $var
* @return $this
*/
public function setName($var)
{
GPBUtil::checkString($var, True);
$this->name = $var;
return $this;
}
/**
* When the event starts. Stored as Timestamp seconds (and optional nanos).
*
* Generated from protobuf field <code>.rv.data.Timestamp start_time = 4;</code>
* @return \Rv\Data\Timestamp|null
*/
public function getStartTime()
{
return $this->start_time;
}
public function hasStartTime()
{
return isset($this->start_time);
}
public function clearStartTime()
{
unset($this->start_time);
}
/**
* When the event starts. Stored as Timestamp seconds (and optional nanos).
*
* Generated from protobuf field <code>.rv.data.Timestamp start_time = 4;</code>
* @param \Rv\Data\Timestamp $var
* @return $this
*/
public function setStartTime($var)
{
GPBUtil::checkMessage($var, \Rv\Data\Timestamp::class);
$this->start_time = $var;
return $this;
}
/**
* Opaque flags blob observed in samples. Often a single byte (e.g. 0x01)
* that ProPresenter uses for recurrence or visibility state.
*
* Generated from protobuf field <code>bytes flags = 5;</code>
* @return string
*/
public function getFlags()
{
return $this->flags;
}
/**
* Opaque flags blob observed in samples. Often a single byte (e.g. 0x01)
* that ProPresenter uses for recurrence or visibility state.
*
* Generated from protobuf field <code>bytes flags = 5;</code>
* @param string $var
* @return $this
*/
public function setFlags($var)
{
GPBUtil::checkString($var, False);
$this->flags = $var;
return $this;
}
/**
* When the event ends. Optional. Same format as `start_time`.
*
* Generated from protobuf field <code>.rv.data.Timestamp end_time = 6;</code>
* @return \Rv\Data\Timestamp|null
*/
public function getEndTime()
{
return $this->end_time;
}
public function hasEndTime()
{
return isset($this->end_time);
}
public function clearEndTime()
{
unset($this->end_time);
}
/**
* When the event ends. Optional. Same format as `start_time`.
*
* Generated from protobuf field <code>.rv.data.Timestamp end_time = 6;</code>
* @param \Rv\Data\Timestamp $var
* @return $this
*/
public function setEndTime($var)
{
GPBUtil::checkMessage($var, \Rv\Data\Timestamp::class);
$this->end_time = $var;
return $this;
}
/**
* Action that runs when the event fires. Encoded as raw protobuf bytes
* (a `rv.data.Action`-shaped message) so the schema can evolve without
* breaking existing clients.
*
* Generated from protobuf field <code>bytes action_data = 8;</code>
* @return string
*/
public function getActionData()
{
return $this->action_data;
}
/**
* Action that runs when the event fires. Encoded as raw protobuf bytes
* (a `rv.data.Action`-shaped message) so the schema can evolve without
* breaking existing clients.
*
* Generated from protobuf field <code>bytes action_data = 8;</code>
* @param string $var
* @return $this
*/
public function setActionData($var)
{
GPBUtil::checkString($var, False);
$this->action_data = $var;
return $this;
}
/**
* Embedded copy of the macro definition the event triggers. Stored as
* raw protobuf bytes (shape compatible with `MacrosDocument.Macro`).
*
* Generated from protobuf field <code>bytes macro_data = 9;</code>
* @return string
*/
public function getMacroData()
{
return $this->macro_data;
}
/**
* Embedded copy of the macro definition the event triggers. Stored as
* raw protobuf bytes (shape compatible with `MacrosDocument.Macro`).
*
* Generated from protobuf field <code>bytes macro_data = 9;</code>
* @param string $var
* @return $this
*/
public function setMacroData($var)
{
GPBUtil::checkString($var, False);
$this->macro_data = $var;
return $this;
}
}

View file

@ -0,0 +1,115 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: keyMappings.proto
namespace Rv\Data;
use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;
/**
* KeyMappingsDocument is the global ProPresenter `KeyMappings` file. The
* reference sample on disk is just an `ApplicationInfo` envelope with an
* otherwise empty body ProPresenter seeds the file at startup. Once the
* user binds hot keys we expect them in `mappings`.
*
* Generated from protobuf message <code>rv.data.KeyMappingsDocument</code>
*/
class KeyMappingsDocument extends \Google\Protobuf\Internal\Message
{
/**
* Application metadata of the writer.
*
* Generated from protobuf field <code>.rv.data.ApplicationInfo application_info = 1;</code>
*/
protected $application_info = null;
/**
* Configured key bindings. Empty in the reference sample.
*
* Generated from protobuf field <code>repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2;</code>
*/
private $mappings;
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type \Rv\Data\ApplicationInfo $application_info
* Application metadata of the writer.
* @type array<\Rv\Data\KeyMappingsDocument\Mapping>|\Google\Protobuf\Internal\RepeatedField $mappings
* Configured key bindings. Empty in the reference sample.
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\KeyMappings::initOnce();
parent::__construct($data);
}
/**
* Application metadata of the writer.
*
* Generated from protobuf field <code>.rv.data.ApplicationInfo application_info = 1;</code>
* @return \Rv\Data\ApplicationInfo|null
*/
public function getApplicationInfo()
{
return $this->application_info;
}
public function hasApplicationInfo()
{
return isset($this->application_info);
}
public function clearApplicationInfo()
{
unset($this->application_info);
}
/**
* Application metadata of the writer.
*
* Generated from protobuf field <code>.rv.data.ApplicationInfo application_info = 1;</code>
* @param \Rv\Data\ApplicationInfo $var
* @return $this
*/
public function setApplicationInfo($var)
{
GPBUtil::checkMessage($var, \Rv\Data\ApplicationInfo::class);
$this->application_info = $var;
return $this;
}
/**
* Configured key bindings. Empty in the reference sample.
*
* Generated from protobuf field <code>repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2;</code>
* @return \Google\Protobuf\Internal\RepeatedField
*/
public function getMappings()
{
return $this->mappings;
}
/**
* Configured key bindings. Empty in the reference sample.
*
* Generated from protobuf field <code>repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2;</code>
* @param array<\Rv\Data\KeyMappingsDocument\Mapping>|\Google\Protobuf\Internal\RepeatedField $var
* @return $this
*/
public function setMappings($var)
{
$arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Rv\Data\KeyMappingsDocument\Mapping::class);
$this->mappings = $arr;
return $this;
}
}

View file

@ -0,0 +1,196 @@
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: keyMappings.proto
namespace Rv\Data\KeyMappingsDocument;
use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;
/**
* Generated from protobuf message <code>rv.data.KeyMappingsDocument.Mapping</code>
*/
class Mapping extends \Google\Protobuf\Internal\Message
{
/**
* Optional stable identifier for the mapping.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
*/
protected $uuid = null;
/**
* The hot key combo that fires the action.
*
* Generated from protobuf field <code>.rv.data.HotKey hot_key = 2;</code>
*/
protected $hot_key = null;
/**
* Action target typically a macro UUID, a control identifier, or any
* other reference ProPresenter chooses to encode. Kept as bytes so we
* round-trip cleanly while ProPresenter's internal schema evolves.
*
* Generated from protobuf field <code>bytes target = 3;</code>
*/
protected $target = '';
/**
* Display name (optional).
*
* Generated from protobuf field <code>string name = 4;</code>
*/
protected $name = '';
/**
* Constructor.
*
* @param array $data {
* Optional. Data for populating the Message object.
*
* @type \Rv\Data\UUID $uuid
* Optional stable identifier for the mapping.
* @type \Rv\Data\HotKey $hot_key
* The hot key combo that fires the action.
* @type string $target
* Action target typically a macro UUID, a control identifier, or any
* other reference ProPresenter chooses to encode. Kept as bytes so we
* round-trip cleanly while ProPresenter's internal schema evolves.
* @type string $name
* Display name (optional).
* }
*/
public function __construct($data = NULL) {
\GPBMetadata\KeyMappings::initOnce();
parent::__construct($data);
}
/**
* Optional stable identifier for the mapping.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
* @return \Rv\Data\UUID|null
*/
public function getUuid()
{
return $this->uuid;
}
public function hasUuid()
{
return isset($this->uuid);
}
public function clearUuid()
{
unset($this->uuid);
}
/**
* Optional stable identifier for the mapping.
*
* Generated from protobuf field <code>.rv.data.UUID uuid = 1;</code>
* @param \Rv\Data\UUID $var
* @return $this
*/
public function setUuid($var)
{
GPBUtil::checkMessage($var, \Rv\Data\UUID::class);
$this->uuid = $var;
return $this;
}
/**
* The hot key combo that fires the action.
*
* Generated from protobuf field <code>.rv.data.HotKey hot_key = 2;</code>
* @return \Rv\Data\HotKey|null
*/
public function getHotKey()
{
return $this->hot_key;
}
public function hasHotKey()
{
return isset($this->hot_key);
}
public function clearHotKey()
{
unset($this->hot_key);
}
/**
* The hot key combo that fires the action.
*
* Generated from protobuf field <code>.rv.data.HotKey hot_key = 2;</code>
* @param \Rv\Data\HotKey $var
* @return $this
*/
public function setHotKey($var)
{
GPBUtil::checkMessage($var, \Rv\Data\HotKey::class);
$this->hot_key = $var;
return $this;
}
/**
* Action target typically a macro UUID, a control identifier, or any
* other reference ProPresenter chooses to encode. Kept as bytes so we
* round-trip cleanly while ProPresenter's internal schema evolves.
*
* Generated from protobuf field <code>bytes target = 3;</code>
* @return string
*/
public function getTarget()
{
return $this->target;
}
/**
* Action target typically a macro UUID, a control identifier, or any
* other reference ProPresenter chooses to encode. Kept as bytes so we
* round-trip cleanly while ProPresenter's internal schema evolves.
*
* Generated from protobuf field <code>bytes target = 3;</code>
* @param string $var
* @return $this
*/
public function setTarget($var)
{
GPBUtil::checkString($var, False);
$this->target = $var;
return $this;
}
/**
* Display name (optional).
*
* Generated from protobuf field <code>string name = 4;</code>
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Display name (optional).
*
* Generated from protobuf field <code>string name = 4;</code>
* @param string $var
* @return $this
*/
public function setName($var)
{
GPBUtil::checkString($var, True);
$this->name = $var;
return $this;
}
}

51
proto/calendar.proto Normal file
View file

@ -0,0 +1,51 @@
syntax = "proto3";
package rv.data;
option cc_enable_arenas = true;
option csharp_namespace = "Pro.SerializationInterop.RVProtoData";
option swift_prefix = "RVData_";
import "rvtimestamp.proto";
import "uuid.proto";
// CalendarDocument is the global ProPresenter `Calendar` file. It holds
// scheduled events that fire macros at given times. The exact semantics of
// the inner sub-messages 8 and 9 (action and embedded macro reference) are
// preserved as raw bytes so the document round-trips byte-for-byte; clients
// that need to inspect them can decode them with `protoc --decode_raw` or
// reach for the raw protobuf message.
message CalendarDocument {
message Event {
// Stable identity of this calendar event.
.rv.data.UUID uuid = 1;
// Display name (e.g. "Doors Open").
string name = 2;
// When the event starts. Stored as Timestamp seconds (and optional nanos).
.rv.data.Timestamp start_time = 4;
// Opaque flags blob observed in samples. Often a single byte (e.g. 0x01)
// that ProPresenter uses for recurrence or visibility state.
bytes flags = 5;
// When the event ends. Optional. Same format as `start_time`.
.rv.data.Timestamp end_time = 6;
// Action that runs when the event fires. Encoded as raw protobuf bytes
// (a `rv.data.Action`-shaped message) so the schema can evolve without
// breaking existing clients.
bytes action_data = 8;
// Embedded copy of the macro definition the event triggers. Stored as
// raw protobuf bytes (shape compatible with `MacrosDocument.Macro`).
bytes macro_data = 9;
}
// Events scheduled in the calendar, in the order ProPresenter wrote them.
repeated Event events = 1;
// Source / mode flag observed in samples (value `1`). Treated as opaque.
uint32 mode = 2;
}

39
proto/keyMappings.proto Normal file
View file

@ -0,0 +1,39 @@
syntax = "proto3";
package rv.data;
option cc_enable_arenas = true;
option csharp_namespace = "Pro.SerializationInterop.RVProtoData";
option swift_prefix = "RVData_";
import "applicationInfo.proto";
import "hotKey.proto";
import "uuid.proto";
// KeyMappingsDocument is the global ProPresenter `KeyMappings` file. The
// reference sample on disk is just an `ApplicationInfo` envelope with an
// otherwise empty body ProPresenter seeds the file at startup. Once the
// user binds hot keys we expect them in `mappings`.
message KeyMappingsDocument {
message Mapping {
// Optional stable identifier for the mapping.
.rv.data.UUID uuid = 1;
// The hot key combo that fires the action.
.rv.data.HotKey hot_key = 2;
// Action target typically a macro UUID, a control identifier, or any
// other reference ProPresenter chooses to encode. Kept as bytes so we
// round-trip cleanly while ProPresenter's internal schema evolves.
bytes target = 3;
// Display name (optional).
string name = 4;
}
// Application metadata of the writer.
.rv.data.ApplicationInfo application_info = 1;
// Configured key bindings. Empty in the reference sample.
repeated Mapping mappings = 2;
}

38
src/CCLIFileReader.php Normal file
View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
use Rv\Data\CCLIDocument;
final class CCLIFileReader
{
public static function read(string $filePath): CCLILibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('CCLI file not found: %s', $filePath));
}
$size = filesize($filePath);
if ($size === false) {
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
}
if ($size === 0) {
throw new RuntimeException(sprintf('CCLI file is empty: %s', $filePath));
}
$data = file_get_contents($filePath);
if ($data === false) {
throw new RuntimeException(sprintf('Unable to read CCLI file: %s', $filePath));
}
$document = new CCLIDocument();
$document->mergeFromString($data);
return new CCLILibrary($document);
}
}

26
src/CCLIFileWriter.php Normal file
View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class CCLIFileWriter
{
public static function write(CCLILibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write CCLI file: %s', $filePath));
}
}
}

103
src/CCLILibrary.php Normal file
View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\ApplicationInfo;
use Rv\Data\CCLIDocument;
use Rv\Data\Template\Slide;
class CCLILibrary
{
public function __construct(
private readonly CCLIDocument $document,
) {
}
public function count(): int
{
return 1;
}
public function isCCLIDisplayEnabled(): bool
{
return $this->document->getEnableCcliDisplay();
}
public function setCCLIDisplayEnabled(bool $enabled): self
{
$this->document->setEnableCcliDisplay($enabled);
return $this;
}
public function getCCLILicense(): string
{
return $this->document->getCcliLicense();
}
public function setCCLILicense(string $license): self
{
$this->document->setCcliLicense($license);
return $this;
}
public function getDisplayType(): int
{
return $this->document->getDisplayType();
}
public function setDisplayType(int $displayType): self
{
$this->document->setDisplayType($displayType);
return $this;
}
public function getTemplate(): ?Slide
{
if (!$this->document->hasTemplate()) {
return null;
}
return $this->document->getTemplate();
}
public function setTemplate(?Slide $template): self
{
if ($template === null) {
$this->document->clearTemplate();
return $this;
}
$this->document->setTemplate($template);
return $this;
}
public function getApplicationInfo(): ?ApplicationInfo
{
return $this->document->getApplicationInfo();
}
public function setApplicationInfo(?ApplicationInfo $applicationInfo): self
{
if ($applicationInfo === null) {
$this->document->clearApplicationInfo();
return $this;
}
$this->document->setApplicationInfo($applicationInfo);
return $this;
}
public function getDocument(): CCLIDocument
{
return $this->document;
}
}

140
src/CalendarEvent.php Normal file
View file

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\CalendarDocument\Event as EventProto;
use Rv\Data\Timestamp;
use Rv\Data\UUID;
class CalendarEvent
{
public function __construct(
private readonly EventProto $event,
) {
}
public function getUuid(): string
{
return $this->event->getUuid()?->getString() ?? '';
}
public function setUuid(string $uuid): self
{
$proto = new UUID();
$proto->setString($uuid);
$this->event->setUuid($proto);
return $this;
}
public function getName(): string
{
return $this->event->getName();
}
public function setName(string $name): self
{
$this->event->setName($name);
return $this;
}
public function getStartTime(): ?Timestamp
{
return $this->event->getStartTime();
}
public function setStartTime(Timestamp $timestamp): self
{
$this->event->setStartTime($timestamp);
return $this;
}
public function getStartTimeSeconds(): ?int
{
$seconds = $this->event->getStartTime()?->getSeconds();
return $seconds === null ? null : (int) $seconds;
}
public function setStartTimeSeconds(int $seconds): self
{
$timestamp = new Timestamp();
$timestamp->setSeconds($seconds);
$this->event->setStartTime($timestamp);
return $this;
}
public function getEndTime(): ?Timestamp
{
return $this->event->getEndTime();
}
public function setEndTime(Timestamp $timestamp): self
{
$this->event->setEndTime($timestamp);
return $this;
}
public function getEndTimeSeconds(): ?int
{
$seconds = $this->event->getEndTime()?->getSeconds();
return $seconds === null ? null : (int) $seconds;
}
public function setEndTimeSeconds(int $seconds): self
{
$timestamp = new Timestamp();
$timestamp->setSeconds($seconds);
$this->event->setEndTime($timestamp);
return $this;
}
public function getFlags(): string
{
return $this->event->getFlags();
}
public function setFlags(string $flags): self
{
$this->event->setFlags($flags);
return $this;
}
public function getActionData(): string
{
return $this->event->getActionData();
}
public function setActionData(string $actionData): self
{
$this->event->setActionData($actionData);
return $this;
}
public function getMacroData(): string
{
return $this->event->getMacroData();
}
public function setMacroData(string $macroData): self
{
$this->event->setMacroData($macroData);
return $this;
}
public function getProto(): EventProto
{
return $this->event;
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
use Rv\Data\CalendarDocument;
final class CalendarFileReader
{
public static function read(string $filePath): CalendarLibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('Calendar file not found: %s', $filePath));
}
$size = filesize($filePath);
if ($size === false) {
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
}
if ($size === 0) {
throw new RuntimeException(sprintf('Calendar file is empty: %s', $filePath));
}
$data = file_get_contents($filePath);
if ($data === false) {
throw new RuntimeException(sprintf('Unable to read Calendar file: %s', $filePath));
}
$document = new CalendarDocument();
$document->mergeFromString($data);
return new CalendarLibrary($document);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class CalendarFileWriter
{
public static function write(CalendarLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$writtenBytes = file_put_contents($filePath, $library->getDocument()->serializeToString());
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write Calendar file: %s', $filePath));
}
}
}

127
src/CalendarLibrary.php Normal file
View file

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\CalendarDocument;
use Rv\Data\CalendarDocument\Event as EventProto;
use Rv\Data\UUID;
class CalendarLibrary
{
/** @var CalendarEvent[] */
private array $events = [];
/** @var array<string, CalendarEvent> */
private array $eventsByUuid = [];
/** @var array<string, CalendarEvent> */
private array $eventsByName = [];
public function __construct(
private readonly CalendarDocument $document,
) {
$this->rebuildIndex();
}
/**
* @return CalendarEvent[]
*/
public function getEvents(): array
{
return $this->events;
}
public function count(): int
{
return count($this->events);
}
public function getEventByUuid(string $uuid): ?CalendarEvent
{
return $this->eventsByUuid[strtoupper($uuid)] ?? null;
}
public function getEventByName(string $name): ?CalendarEvent
{
return $this->eventsByName[$name] ?? null;
}
public function addEvent(string $name, string $uuid): CalendarEvent
{
$proto = new EventProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$existing = iterator_to_array($this->document->getEvents());
$existing[] = $proto;
$this->document->setEvents($existing);
$this->rebuildIndex();
return $this->getEventByUuid($uuid) ?? new CalendarEvent($proto);
}
public function removeEvent(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getEvents() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setEvents($kept);
$this->rebuildIndex();
return true;
}
public function getMode(): int
{
return $this->document->getMode();
}
public function setMode(int $mode): void
{
$this->document->setMode($mode);
}
public function getDocument(): CalendarDocument
{
return $this->document;
}
private function rebuildIndex(): void
{
$this->events = [];
$this->eventsByUuid = [];
$this->eventsByName = [];
foreach ($this->document->getEvents() as $proto) {
$event = new CalendarEvent($proto);
$this->events[] = $event;
$uuid = strtoupper($event->getUuid());
if ($uuid !== '') {
$this->eventsByUuid[$uuid] = $event;
}
$name = $event->getName();
if ($name !== '') {
$this->eventsByName[$name] ??= $event;
}
}
}
}

View file

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\ClearGroupsDocument\ClearGroup as ClearGroupProto;
use Rv\Data\Color;
use Rv\Data\UUID;
class ClearGroupDefinition
{
public function __construct(
private readonly ClearGroupProto $group,
) {
}
public function getUuid(): string
{
return $this->group->getUuid()?->getString() ?? '';
}
public function setUuid(string $uuid): self
{
$proto = new UUID();
$proto->setString($uuid);
$this->group->setUuid($proto);
return $this;
}
public function getName(): string
{
return $this->group->getName();
}
public function setName(string $name): self
{
$this->group->setName($name);
return $this;
}
/**
* @return int[]
*/
public function getLayerTargets(): array
{
return iterator_to_array($this->group->getLayerTargets());
}
/**
* @param int[] $targets
*/
public function setLayerTargets(array $targets): self
{
$this->group->setLayerTargets($targets);
return $this;
}
public function isHiddenInPreview(): bool
{
return $this->group->getIsHiddenInPreview();
}
public function setHiddenInPreview(bool $hidden): self
{
$this->group->setIsHiddenInPreview($hidden);
return $this;
}
public function getImageData(): string
{
return $this->group->getImageData();
}
public function setImageData(string $imageData): self
{
$this->group->setImageData($imageData);
return $this;
}
public function getImageType(): int
{
return $this->group->getImageType();
}
public function setImageType(int $imageType): self
{
$this->group->setImageType($imageType);
return $this;
}
public function isIconTinted(): bool
{
return $this->group->getIsIconTinted();
}
public function setIconTinted(bool $tinted): self
{
$this->group->setIsIconTinted($tinted);
return $this;
}
/**
* @return array{r: float, g: float, b: float, a: float}|null
*/
public function getColor(): ?array
{
if (!$this->group->hasIconTintColor()) {
return null;
}
$color = $this->group->getIconTintColor();
return [
'r' => $color->getRed(),
'g' => $color->getGreen(),
'b' => $color->getBlue(),
'a' => $color->getAlpha(),
];
}
public function getColorHex(): ?string
{
$color = $this->getColor();
if ($color === null) {
return null;
}
return sprintf(
'#%02X%02X%02X',
(int) round(max(0.0, min(1.0, $color['r'])) * 255),
(int) round(max(0.0, min(1.0, $color['g'])) * 255),
(int) round(max(0.0, min(1.0, $color['b'])) * 255),
);
}
/**
* @param array{r: float, g: float, b: float, a?: float}|null $color
*/
public function setColor(?array $color): self
{
if ($color === null) {
$this->group->clearIconTintColor();
return $this;
}
$proto = new Color();
$proto->setRed((float) $color['r']);
$proto->setGreen((float) $color['g']);
$proto->setBlue((float) $color['b']);
$proto->setAlpha((float) ($color['a'] ?? 1.0));
$this->group->setIconTintColor($proto);
return $this;
}
/**
* @return int[]
*/
public function getTimelineTargets(): array
{
return iterator_to_array($this->group->getTimelineTargets());
}
/**
* @param int[] $targets
*/
public function setTimelineTargets(array $targets): self
{
$this->group->setTimelineTargets($targets);
return $this;
}
public function clearsPresentationNextSlide(): bool
{
return $this->group->getClearPresentationNextSlide();
}
public function setClearPresentationNextSlide(bool $clear): self
{
$this->group->setClearPresentationNextSlide($clear);
return $this;
}
public function getProto(): ClearGroupProto
{
return $this->group;
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
use Rv\Data\ClearGroupsDocument;
final class ClearGroupsFileReader
{
public static function read(string $filePath): ClearGroupsLibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('ClearGroups file not found: %s', $filePath));
}
$size = filesize($filePath);
if ($size === false) {
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
}
if ($size === 0) {
throw new RuntimeException(sprintf('ClearGroups file is empty: %s', $filePath));
}
$data = file_get_contents($filePath);
if ($data === false) {
throw new RuntimeException(sprintf('Unable to read ClearGroups file: %s', $filePath));
}
$document = new ClearGroupsDocument();
$document->mergeFromString($data);
return new ClearGroupsLibrary($document);
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class ClearGroupsFileWriter
{
public static function write(ClearGroupsLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write ClearGroups file: %s', $filePath));
}
}
}

119
src/ClearGroupsLibrary.php Normal file
View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\ClearGroupsDocument;
use Rv\Data\ClearGroupsDocument\ClearGroup as ClearGroupProto;
use Rv\Data\UUID;
class ClearGroupsLibrary
{
/** @var ClearGroupDefinition[] */
private array $groups = [];
/** @var array<string, ClearGroupDefinition> */
private array $groupsByUuid = [];
/** @var array<string, ClearGroupDefinition> */
private array $groupsByName = [];
public function __construct(
private readonly ClearGroupsDocument $document,
) {
$this->rebuildIndex();
}
/**
* Return clear groups in document order.
*
* @return ClearGroupDefinition[]
*/
public function getGroups(): array
{
return $this->groups;
}
public function count(): int
{
return count($this->groups);
}
public function getClearGroupByUuid(string $uuid): ?ClearGroupDefinition
{
return $this->groupsByUuid[strtoupper($uuid)] ?? null;
}
public function getClearGroupByName(string $name): ?ClearGroupDefinition
{
return $this->groupsByName[$name] ?? null;
}
public function addClearGroup(string $name, string $uuid): ClearGroupDefinition
{
$proto = new ClearGroupProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$existing = iterator_to_array($this->document->getGroups());
$existing[] = $proto;
$this->document->setGroups($existing);
$this->rebuildIndex();
return $this->getClearGroupByUuid($uuid) ?? new ClearGroupDefinition($proto);
}
public function removeClearGroup(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getGroups() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setGroups($kept);
$this->rebuildIndex();
return true;
}
public function getDocument(): ClearGroupsDocument
{
return $this->document;
}
private function rebuildIndex(): void
{
$this->groups = [];
$this->groupsByUuid = [];
$this->groupsByName = [];
foreach ($this->document->getGroups() as $proto) {
$group = new ClearGroupDefinition($proto);
$this->groups[] = $group;
$uuid = strtoupper($group->getUuid());
if ($uuid !== '') {
$this->groupsByUuid[$uuid] = $group;
}
$name = $group->getName();
if ($name !== '') {
$this->groupsByName[$name] ??= $group;
}
}
}
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
class CommunicationDevice
{
/** @var array<string, mixed> */
private array $fields;
/**
* @param array<string, mixed>|null $fields
*/
public function __construct(?array $fields = null)
{
$this->fields = $fields ?? [];
}
public function getId(): string
{
return (string) ($this->fields['id'] ?? '');
}
public function setId(string $id): self
{
$this->fields['id'] = $id;
return $this;
}
public function getName(): string
{
return (string) ($this->fields['name'] ?? '');
}
public function setName(string $name): self
{
$this->fields['name'] = $name;
return $this;
}
public function getType(): string
{
return (string) ($this->fields['type'] ?? '');
}
public function setType(string $type): self
{
$this->fields['type'] = $type;
return $this;
}
public function getAddress(): string
{
return (string) ($this->fields['address'] ?? '');
}
public function setAddress(string $address): self
{
$this->fields['address'] = $address;
return $this;
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return $this->fields;
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class CommunicationDevicesFileReader
{
public static function read(string $filePath): CommunicationDevicesLibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('CommunicationDevices file not found: %s', $filePath));
}
$contents = file_get_contents($filePath);
if ($contents === false) {
throw new RuntimeException(sprintf('Unable to read CommunicationDevices file: %s', $filePath));
}
return CommunicationDevicesLibrary::fromJson($contents);
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class CommunicationDevicesFileWriter
{
public static function write(CommunicationDevicesLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$json = $library->toJson(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$writtenBytes = file_put_contents($filePath, $json);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write CommunicationDevices file: %s', $filePath));
}
}
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use RuntimeException;
class CommunicationDevicesLibrary
{
/** @var CommunicationDevice[] */
private array $devices = [];
/**
* @param CommunicationDevice[] $devices
*/
public function __construct(array $devices = [])
{
$this->devices = array_values($devices);
}
public static function fromJson(string $json): self
{
$decoded = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Unable to decode CommunicationDevices JSON: ' . json_last_error_msg());
}
if (!is_array($decoded)) {
throw new RuntimeException('CommunicationDevices JSON must decode to an array.');
}
$devices = [];
foreach ($decoded as $entry) {
if (is_array($entry)) {
/** @var array<string, mixed> $entry */
$devices[] = new CommunicationDevice($entry);
}
}
return new self($devices);
}
public function toJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES): string
{
$json = json_encode($this->getDocument(), $flags);
if ($json === false) {
throw new RuntimeException('Unable to encode CommunicationDevices JSON: ' . json_last_error_msg());
}
return $json;
}
/**
* @return array<int, array<string, mixed>>
*/
public function getDocument(): array
{
return array_map(static fn (CommunicationDevice $device): array => $device->toArray(), $this->devices);
}
/**
* @return CommunicationDevice[]
*/
public function getDevices(): array
{
return $this->devices;
}
public function addDevice(CommunicationDevice $device): CommunicationDevice
{
$this->devices[] = $device;
return $device;
}
public function removeDevice(string $id): bool
{
foreach ($this->devices as $index => $device) {
if ($device->getId() === $id) {
array_splice($this->devices, $index, 1);
return true;
}
}
return false;
}
public function count(): int
{
return count($this->devices);
}
}

119
src/GroupDefinition.php Normal file
View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\Color;
use Rv\Data\Group as GroupProto;
use Rv\Data\HotKey;
use Rv\Data\UUID;
class GroupDefinition
{
public function __construct(
private readonly GroupProto $group,
) {
}
public function getUuid(): string
{
return $this->group->getUuid()?->getString() ?? '';
}
public function setUuid(string $uuid): self
{
$proto = new UUID();
$proto->setString($uuid);
$this->group->setUuid($proto);
return $this;
}
public function getName(): string
{
return $this->group->getName();
}
public function setName(string $name): self
{
$this->group->setName($name);
return $this;
}
/**
* @return array{r: float, g: float, b: float, a: float}|null
*/
public function getColor(): ?array
{
if (!$this->group->hasColor()) {
return null;
}
$color = $this->group->getColor();
return [
'r' => $color->getRed(),
'g' => $color->getGreen(),
'b' => $color->getBlue(),
'a' => $color->getAlpha(),
];
}
public function getColorHex(): ?string
{
$color = $this->getColor();
if ($color === null) {
return null;
}
return sprintf(
'#%02X%02X%02X',
(int) round(max(0.0, min(1.0, $color['r'])) * 255),
(int) round(max(0.0, min(1.0, $color['g'])) * 255),
(int) round(max(0.0, min(1.0, $color['b'])) * 255),
);
}
/**
* @param array{r: float, g: float, b: float, a?: float}|null $color
*/
public function setColor(?array $color): self
{
if ($color === null) {
$this->group->clearColor();
return $this;
}
$proto = new Color();
$proto->setRed((float) $color['r']);
$proto->setGreen((float) $color['g']);
$proto->setBlue((float) $color['b']);
$proto->setAlpha((float) ($color['a'] ?? 1.0));
$this->group->setColor($proto);
return $this;
}
public function getHotKey(): ?HotKey
{
return $this->group->getHotKey();
}
public function getApplicationGroupName(): string
{
return $this->group->getApplicationGroupName();
}
public function getApplicationGroupUuid(): string
{
return $this->group->getApplicationGroupIdentifier()?->getString() ?? '';
}
public function getProto(): GroupProto
{
return $this->group;
}
}

117
src/GroupLibrary.php Normal file
View file

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\Group as GroupProto;
use Rv\Data\ProGroupsDocument;
use Rv\Data\UUID;
class GroupLibrary
{
/** @var GroupDefinition[] */
private array $groups = [];
/** @var array<string, GroupDefinition> */
private array $groupsByUuid = [];
/** @var array<string, GroupDefinition> */
private array $groupsByName = [];
public function __construct(
private readonly ProGroupsDocument $document,
) {
$this->rebuildIndex();
}
/**
* @return GroupDefinition[]
*/
public function getGroups(): array
{
return $this->groups;
}
public function count(): int
{
return count($this->groups);
}
public function getGroupByUuid(string $uuid): ?GroupDefinition
{
return $this->groupsByUuid[strtoupper($uuid)] ?? null;
}
public function getGroupByName(string $name): ?GroupDefinition
{
return $this->groupsByName[$name] ?? null;
}
public function addGroup(string $name, string $uuid): GroupDefinition
{
$proto = new GroupProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$existing = iterator_to_array($this->document->getGroups());
$existing[] = $proto;
$this->document->setGroups($existing);
$this->rebuildIndex();
return $this->getGroupByUuid($uuid) ?? new GroupDefinition($proto);
}
public function removeGroup(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getGroups() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setGroups($kept);
$this->rebuildIndex();
return true;
}
public function getDocument(): ProGroupsDocument
{
return $this->document;
}
private function rebuildIndex(): void
{
$this->groups = [];
$this->groupsByUuid = [];
$this->groupsByName = [];
foreach ($this->document->getGroups() as $proto) {
$group = new GroupDefinition($proto);
$this->groups[] = $group;
$uuid = strtoupper($group->getUuid());
if ($uuid !== '') {
$this->groupsByUuid[$uuid] = $group;
}
$name = $group->getName();
if ($name !== '') {
$this->groupsByName[$name] ??= $group;
}
}
}
}

38
src/GroupsFileReader.php Normal file
View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
use Rv\Data\ProGroupsDocument;
final class GroupsFileReader
{
public static function read(string $filePath): GroupLibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('Groups file not found: %s', $filePath));
}
$size = filesize($filePath);
if ($size === false) {
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
}
if ($size === 0) {
throw new RuntimeException(sprintf('Groups file is empty: %s', $filePath));
}
$data = file_get_contents($filePath);
if ($data === false) {
throw new RuntimeException(sprintf('Unable to read Groups file: %s', $filePath));
}
$document = new ProGroupsDocument();
$document->mergeFromString($data);
return new GroupLibrary($document);
}
}

26
src/GroupsFileWriter.php Normal file
View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class GroupsFileWriter
{
public static function write(GroupLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write Groups file: %s', $filePath));
}
}
}

82
src/KeyMapping.php Normal file
View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\HotKey;
use Rv\Data\KeyMappingsDocument\Mapping as MappingProto;
use Rv\Data\UUID;
class KeyMapping
{
public function __construct(
private readonly MappingProto $mapping,
) {
}
public function getUuid(): string
{
return $this->mapping->getUuid()?->getString() ?? '';
}
public function setUuid(string $uuid): self
{
$proto = new UUID();
$proto->setString($uuid);
$this->mapping->setUuid($proto);
return $this;
}
public function getName(): string
{
return $this->mapping->getName();
}
public function setName(string $name): self
{
$this->mapping->setName($name);
return $this;
}
public function getHotKey(): ?HotKey
{
if (!$this->mapping->hasHotKey()) {
return null;
}
return $this->mapping->getHotKey();
}
public function setHotKey(?HotKey $hotKey): self
{
if ($hotKey === null) {
$this->mapping->clearHotKey();
return $this;
}
$this->mapping->setHotKey($hotKey);
return $this;
}
public function getTarget(): string
{
return $this->mapping->getTarget();
}
public function setTarget(string $target): self
{
$this->mapping->setTarget($target);
return $this;
}
public function getProto(): MappingProto
{
return $this->mapping;
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
use Rv\Data\KeyMappingsDocument;
final class KeyMappingsFileReader
{
public static function read(string $filePath): KeyMappingsLibrary
{
if ($filePath === '' || !is_file($filePath)) {
throw new InvalidArgumentException(sprintf('KeyMappings file not found: %s', $filePath));
}
$size = filesize($filePath);
if ($size === false) {
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
}
if ($size === 0) {
throw new RuntimeException(sprintf('KeyMappings file is empty: %s', $filePath));
}
$data = file_get_contents($filePath);
if ($data === false) {
throw new RuntimeException(sprintf('Unable to read KeyMappings file: %s', $filePath));
}
$document = new KeyMappingsDocument();
$document->mergeFromString($data);
return new KeyMappingsLibrary($document);
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class KeyMappingsFileWriter
{
public static function write(KeyMappingsLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write KeyMappings file: %s', $filePath));
}
}
}

139
src/KeyMappingsLibrary.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\ApplicationInfo;
use Rv\Data\KeyMappingsDocument;
use Rv\Data\KeyMappingsDocument\Mapping as MappingProto;
use Rv\Data\UUID;
class KeyMappingsLibrary
{
/** @var KeyMapping[] */
private array $mappings = [];
/** @var array<string, KeyMapping> */
private array $mappingsByUuid = [];
/** @var array<string, KeyMapping> */
private array $mappingsByName = [];
public function __construct(
private readonly KeyMappingsDocument $document,
) {
$this->rebuildIndex();
}
/**
* Return key mappings in document order.
*
* @return KeyMapping[]
*/
public function getMappings(): array
{
return $this->mappings;
}
public function count(): int
{
return count($this->mappings);
}
public function getMappingByUuid(string $uuid): ?KeyMapping
{
return $this->mappingsByUuid[strtoupper($uuid)] ?? null;
}
public function getMappingByName(string $name): ?KeyMapping
{
return $this->mappingsByName[$name] ?? null;
}
public function addMapping(string $name, string $uuid, string $target = ''): KeyMapping
{
$proto = new MappingProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$proto->setTarget($target);
$existing = iterator_to_array($this->document->getMappings());
$existing[] = $proto;
$this->document->setMappings($existing);
$this->rebuildIndex();
return $this->getMappingByUuid($uuid) ?? new KeyMapping($proto);
}
public function removeMapping(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getMappings() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setMappings($kept);
$this->rebuildIndex();
return true;
}
public function getApplicationInfo(): ?ApplicationInfo
{
return $this->document->getApplicationInfo();
}
public function setApplicationInfo(?ApplicationInfo $applicationInfo): self
{
if ($applicationInfo === null) {
$this->document->clearApplicationInfo();
return $this;
}
$this->document->setApplicationInfo($applicationInfo);
return $this;
}
public function getDocument(): KeyMappingsDocument
{
return $this->document;
}
private function rebuildIndex(): void
{
$this->mappings = [];
$this->mappingsByUuid = [];
$this->mappingsByName = [];
foreach ($this->document->getMappings() as $proto) {
$mapping = new KeyMapping($proto);
$this->mappings[] = $mapping;
$uuid = strtoupper($mapping->getUuid());
if ($uuid !== '') {
$this->mappingsByUuid[$uuid] = $mapping;
}
$name = $mapping->getName();
if ($name !== '') {
$this->mappingsByName[$name] ??= $mapping;
}
}
}
}

View file

@ -4,15 +4,10 @@ declare(strict_types=1);
namespace ProPresenter\Parser; namespace ProPresenter\Parser;
use InvalidArgumentException;
use Rv\Data\Action\Label as LabelProto; use Rv\Data\Action\Label as LabelProto;
use Rv\Data\Color;
/**
* Wraps a protobuf {@see LabelProto} from the global ProPresenter `Labels`
* file. Surfaces the label's display name (the protobuf `text` field) and an
* optional color used by the UI to tint slides / cues that carry the label.
*
* Labels do not have a UUID identity is the name string.
*/
class Label class Label
{ {
public function __construct( public function __construct(
@ -20,30 +15,24 @@ class Label
) { ) {
} }
/**
* Display name (the protobuf `text` field; this is what the
* ProPresenter UI renders next to the swatch).
*/
public function getName(): string public function getName(): string
{ {
return $this->label->getText(); return $this->label->getText();
} }
/** public function setName(string $name): self
* Whether the label carries an explicit color message (the absence of a {
* color is not the same as black: ProPresenter renders unset labels with $this->label->setText($name);
* the default UI color).
*/ return $this;
}
public function hasColor(): bool public function hasColor(): bool
{ {
return $this->label->hasColor(); return $this->label->hasColor();
} }
/** /**
* Get the label's color as an associative array, or null when unset.
*
* Channels are floats in the 0..1 range as stored by ProPresenter.
*
* @return array{r: float, g: float, b: float, a: float}|null * @return array{r: float, g: float, b: float, a: float}|null
*/ */
public function getColor(): ?array public function getColor(): ?array
@ -62,10 +51,6 @@ class Label
]; ];
} }
/**
* Convenience: render the color as a 6-digit `#RRGGBB` hex string,
* dropping alpha. Returns null when the label has no color.
*/
public function getColorHex(): ?string public function getColorHex(): ?string
{ {
$color = $this->getColor(); $color = $this->getColor();
@ -82,8 +67,48 @@ class Label
} }
/** /**
* Get the underlying protobuf Label message. * Set the label's color. Pass `null` to remove the color (the UI will
* fall back to its default tint).
*
* @param array{r: float, g: float, b: float, a?: float}|null $color
*/ */
public function setColor(?array $color): self
{
if ($color === null) {
$this->label->clearColor();
return $this;
}
$proto = new Color();
$proto->setRed((float) $color['r']);
$proto->setGreen((float) $color['g']);
$proto->setBlue((float) $color['b']);
$proto->setAlpha((float) ($color['a'] ?? 1.0));
$this->label->setColor($proto);
return $this;
}
/**
* Convenience setter: accepts a `#RRGGBB` or `#RRGGBBAA` hex value and
* applies it to the label. Alpha defaults to 1.0 when missing.
*/
public function setColorHex(string $hex): self
{
$hex = ltrim($hex, '#');
if (!preg_match('/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/', $hex)) {
throw new InvalidArgumentException(sprintf('Invalid hex color: %s', $hex));
}
$r = hexdec(substr($hex, 0, 2)) / 255.0;
$g = hexdec(substr($hex, 2, 2)) / 255.0;
$b = hexdec(substr($hex, 4, 2)) / 255.0;
$a = strlen($hex) === 8 ? hexdec(substr($hex, 6, 2)) / 255.0 : 1.0;
return $this->setColor(['r' => $r, 'g' => $g, 'b' => $b, 'a' => $a]);
}
public function getProto(): LabelProto public function getProto(): LabelProto
{ {
return $this->label; return $this->label;

View file

@ -4,17 +4,10 @@ declare(strict_types=1);
namespace ProPresenter\Parser; namespace ProPresenter\Parser;
use Rv\Data\Action\Label as LabelProto;
use Rv\Data\Color;
use Rv\Data\ProLabelsDocument; use Rv\Data\ProLabelsDocument;
/**
* Wraps the protobuf {@see ProLabelsDocument} the global ProPresenter
* `Labels` file which lists every named label and its UI color.
*
* Labels are identified by name only (there is no UUID field). Names are
* expected to be unique inside the document; if the source file violates
* that, the first occurrence wins for {@see getLabelByName()} but every
* label is preserved in {@see getLabels()} in document order.
*/
class LabelLibrary class LabelLibrary
{ {
/** @var Label[] */ /** @var Label[] */
@ -30,16 +23,7 @@ class LabelLibrary
private readonly ProLabelsDocument $document, private readonly ProLabelsDocument $document,
) { ) {
foreach ($this->document->getLabels() as $labelProto) { foreach ($this->document->getLabels() as $labelProto) {
$label = new Label($labelProto); $this->register(new Label($labelProto));
$this->labels[] = $label;
$name = $label->getName();
if ($name === '') {
continue;
}
$this->labelsByName[$name] ??= $label;
$this->labelsByNameLower[strtolower($name)] ??= $label;
} }
} }
@ -56,28 +40,95 @@ class LabelLibrary
return count($this->labels); return count($this->labels);
} }
/**
* Exact-name lookup. Use {@see findLabelByName()} for a
* case-insensitive variant.
*/
public function getLabelByName(string $name): ?Label public function getLabelByName(string $name): ?Label
{ {
return $this->labelsByName[$name] ?? null; return $this->labelsByName[$name] ?? null;
} }
/**
* Case-insensitive name lookup.
*/
public function findLabelByName(string $name): ?Label public function findLabelByName(string $name): ?Label
{ {
return $this->labelsByNameLower[strtolower($name)] ?? null; return $this->labelsByNameLower[strtolower($name)] ?? null;
} }
/** /**
* Get the underlying protobuf ProLabelsDocument. * Append a brand-new label to the document.
*
* @param array{r: float, g: float, b: float, a?: float}|null $color
*/ */
public function addLabel(string $name, ?array $color = null): Label
{
$proto = new LabelProto();
$proto->setText($name);
if ($color !== null) {
$colorProto = new Color();
$colorProto->setRed((float) $color['r']);
$colorProto->setGreen((float) $color['g']);
$colorProto->setBlue((float) $color['b']);
$colorProto->setAlpha((float) ($color['a'] ?? 1.0));
$proto->setColor($colorProto);
}
$existing = iterator_to_array($this->document->getLabels());
$existing[] = $proto;
$this->document->setLabels($existing);
$label = new Label($proto);
$this->register($label);
return $label;
}
/**
* Remove a label by its current name. Returns true when something was
* removed.
*/
public function removeLabel(string $name): bool
{
$kept = [];
$removed = false;
foreach ($this->document->getLabels() as $proto) {
if (!$removed && $proto->getText() === $name) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setLabels($kept);
$this->rebuildIndex();
return true;
}
public function getDocument(): ProLabelsDocument public function getDocument(): ProLabelsDocument
{ {
return $this->document; return $this->document;
} }
private function register(Label $label): void
{
$this->labels[] = $label;
$name = $label->getName();
if ($name === '') {
return;
}
$this->labelsByName[$name] ??= $label;
$this->labelsByNameLower[strtolower($name)] ??= $label;
}
private function rebuildIndex(): void
{
$this->labels = [];
$this->labelsByName = [];
$this->labelsByNameLower = [];
foreach ($this->document->getLabels() as $proto) {
$this->register(new Label($proto));
}
}
} }

26
src/LabelsFileWriter.php Normal file
View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class LabelsFileWriter
{
public static function write(LabelLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write Labels file: %s', $filePath));
}
}
}

View file

@ -4,16 +4,10 @@ declare(strict_types=1);
namespace ProPresenter\Parser; namespace ProPresenter\Parser;
use Rv\Data\Color;
use Rv\Data\MacrosDocument\Macro as MacroProto; use Rv\Data\MacrosDocument\Macro as MacroProto;
use Rv\Data\UUID;
/**
* Wraps a protobuf Macro, exposing its identifying fields (UUID, name) plus
* convenience metadata (color, action count, image type, startup flag).
*
* Macros live in the global ProPresenter `Macros` document and may belong to
* one or more {@see MacroCollection}s. Membership is resolved by
* {@see MacroLibrary}, not by this wrapper.
*/
class Macro class Macro
{ {
public function __construct( public function __construct(
@ -21,25 +15,33 @@ class Macro
) { ) {
} }
/**
* Get the macro's UUID as an upper-case string (empty when unset).
*/
public function getUuid(): string public function getUuid(): string
{ {
return $this->macro->getUuid()?->getString() ?? ''; return $this->macro->getUuid()?->getString() ?? '';
} }
/** public function setUuid(string $uuid): self
* Get the macro's display name. {
*/ $proto = new UUID();
$proto->setString($uuid);
$this->macro->setUuid($proto);
return $this;
}
public function getName(): string public function getName(): string
{ {
return $this->macro->getName(); return $this->macro->getName();
} }
public function setName(string $name): self
{
$this->macro->setName($name);
return $this;
}
/** /**
* Get the macro's color as an associative array, or null when unset.
*
* @return array{r: float, g: float, b: float, a: float}|null * @return array{r: float, g: float, b: float, a: float}|null
*/ */
public function getColor(): ?array public function getColor(): ?array
@ -59,35 +61,71 @@ class Macro
} }
/** /**
* Whether the macro is configured to fire on application startup. * @param array{r: float, g: float, b: float, a?: float}|null $color
*/ */
public function setColor(?array $color): self
{
if ($color === null) {
$this->macro->clearColor();
return $this;
}
$proto = new Color();
$proto->setRed((float) $color['r']);
$proto->setGreen((float) $color['g']);
$proto->setBlue((float) $color['b']);
$proto->setAlpha((float) ($color['a'] ?? 1.0));
$this->macro->setColor($proto);
return $this;
}
public function getTriggerOnStartup(): bool public function getTriggerOnStartup(): bool
{ {
return $this->macro->getTriggerOnStartup(); return $this->macro->getTriggerOnStartup();
} }
/** public function setTriggerOnStartup(bool $value): self
* Number of action entries attached to this macro. {
* $this->macro->setTriggerOnStartup($value);
* Action payloads are not exposed by this wrapper; use {@see getProto()} to
* inspect them via the generated protobuf classes. return $this;
*/ }
public function getActionCount(): int public function getActionCount(): int
{ {
return count($this->macro->getActions()); return count($this->macro->getActions());
} }
/**
* Icon enum value (see {@see \Rv\Data\MacrosDocument\Macro\ImageType}).
*/
public function getImageType(): int public function getImageType(): int
{ {
return $this->macro->getImageType(); return $this->macro->getImageType();
} }
public function setImageType(int $value): self
{
$this->macro->setImageType($value);
return $this;
}
/** /**
* Get the underlying protobuf Macro message. * Custom icon bytes (PNG/JPG). Empty when ProPresenter uses one of the
* built-in `image_type` icons.
*/ */
public function getImageData(): string
{
return $this->macro->getImageData();
}
public function setImageData(string $bytes): self
{
$this->macro->setImageData($bytes);
return $this;
}
public function getProto(): MacroProto public function getProto(): MacroProto
{ {
return $this->macro; return $this->macro;

View file

@ -5,11 +5,9 @@ declare(strict_types=1);
namespace ProPresenter\Parser; namespace ProPresenter\Parser;
use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto; use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto;
use Rv\Data\MacrosDocument\MacroCollection\Item as ItemProto;
use Rv\Data\UUID;
/**
* Wraps a protobuf MacroCollection. A collection groups macros by UUID
* reference; macro definitions themselves live at the document root.
*/
class MacroCollection class MacroCollection
{ {
public function __construct( public function __construct(
@ -17,28 +15,33 @@ class MacroCollection
) { ) {
} }
/**
* Get the collection's UUID as a string (empty when unset).
*/
public function getUuid(): string public function getUuid(): string
{ {
return $this->collection->getUuid()?->getString() ?? ''; return $this->collection->getUuid()?->getString() ?? '';
} }
/** public function setUuid(string $uuid): self
* Get the collection's display name (e.g. "Ablauf"). {
*/ $proto = new UUID();
$proto->setString($uuid);
$this->collection->setUuid($proto);
return $this;
}
public function getName(): string public function getName(): string
{ {
return $this->collection->getName(); return $this->collection->getName();
} }
public function setName(string $name): self
{
$this->collection->setName($name);
return $this;
}
/** /**
* Get the UUIDs of macros referenced by this collection, in order.
*
* Items in the protobuf use a `oneof` ItemType currently only
* `macro_id` is defined. Items without a populated reference are skipped.
*
* @return string[] * @return string[]
*/ */
public function getMacroUuids(): array public function getMacroUuids(): array
@ -55,8 +58,39 @@ class MacroCollection
} }
/** /**
* Get the underlying protobuf MacroCollection message. * Replace the collection's referenced macro UUIDs in one call. Pass UUID
* strings exactly as ProPresenter writes them (upper-case is conventional).
*
* @param string[] $uuids
*/ */
public function setMacroUuids(array $uuids): self
{
$items = [];
foreach ($uuids as $uuid) {
$item = new ItemProto();
$ref = new UUID();
$ref->setString($uuid);
$item->setMacroId($ref);
$items[] = $item;
}
$this->collection->setItems($items);
return $this;
}
public function addMacroUuid(string $uuid): self
{
$items = iterator_to_array($this->collection->getItems());
$item = new ItemProto();
$ref = new UUID();
$ref->setString($uuid);
$item->setMacroId($ref);
$items[] = $item;
$this->collection->setItems($items);
return $this;
}
public function getProto(): MacroCollectionProto public function getProto(): MacroCollectionProto
{ {
return $this->collection; return $this->collection;

View file

@ -5,16 +5,10 @@ declare(strict_types=1);
namespace ProPresenter\Parser; namespace ProPresenter\Parser;
use Rv\Data\MacrosDocument; use Rv\Data\MacrosDocument;
use Rv\Data\MacrosDocument\Macro as MacroProto;
use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto;
use Rv\Data\UUID;
/**
* Wraps a protobuf MacrosDocument the global ProPresenter `Macros` file
* which lists every macro definition and the collections that group them.
*
* Lookup helpers index macros and collections by UUID (case-insensitive) and
* by name, mirroring the convention used by {@see Song}. Collection
* membership is resolved by mapping {@see MacroCollection::getMacroUuids()}
* back through this library.
*/
class MacroLibrary class MacroLibrary
{ {
/** @var Macro[] */ /** @var Macro[] */
@ -41,43 +35,7 @@ class MacroLibrary
public function __construct( public function __construct(
private readonly MacrosDocument $document, private readonly MacrosDocument $document,
) { ) {
foreach ($this->document->getMacros() as $macroProto) { $this->rebuildIndex();
$macro = new Macro($macroProto);
$this->macros[] = $macro;
$uuid = strtoupper($macro->getUuid());
if ($uuid !== '') {
$this->macrosByUuid[$uuid] = $macro;
}
$name = $macro->getName();
if ($name !== '') {
$this->macrosByName[$name] = $macro;
}
}
foreach ($this->document->getMacroCollections() as $collectionProto) {
$collection = new MacroCollection($collectionProto);
$this->collections[] = $collection;
$uuid = strtoupper($collection->getUuid());
if ($uuid !== '') {
$this->collectionsByUuid[$uuid] = $collection;
}
$name = $collection->getName();
if ($name !== '') {
$this->collectionsByName[$name] = $collection;
}
foreach ($collection->getMacroUuids() as $macroUuid) {
$key = strtoupper($macroUuid);
if ($key === '') {
continue;
}
$this->collectionsByMacroUuid[$key][] = $collection;
}
}
} }
/** /**
@ -117,10 +75,6 @@ class MacroLibrary
} }
/** /**
* Resolve a collection's referenced macros to {@see Macro} wrappers.
*
* Unknown UUIDs (referenced but not defined at document root) are skipped.
*
* @return Macro[] * @return Macro[]
*/ */
public function getMacrosForCollection(MacroCollection $collection): array public function getMacrosForCollection(MacroCollection $collection): array
@ -137,10 +91,6 @@ class MacroLibrary
} }
/** /**
* Get the collections a macro belongs to (membership is by UUID
* reference). A macro may legally appear in zero, one, or many
* collections.
*
* @return MacroCollection[] * @return MacroCollection[]
*/ */
public function getCollectionsForMacro(Macro $macro): array public function getCollectionsForMacro(Macro $macro): array
@ -154,10 +104,141 @@ class MacroLibrary
} }
/** /**
* Get the underlying protobuf MacrosDocument. * Append a brand-new macro to the document.
*/ */
public function addMacro(string $name, string $uuid): Macro
{
$proto = new MacroProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$existing = iterator_to_array($this->document->getMacros());
$existing[] = $proto;
$this->document->setMacros($existing);
$this->rebuildIndex();
return $this->getMacroByUuid($uuid) ?? new Macro($proto);
}
public function removeMacro(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getMacros() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setMacros($kept);
$this->rebuildIndex();
return true;
}
public function addCollection(string $name, string $uuid): MacroCollection
{
$proto = new MacroCollectionProto();
$uuidProto = new UUID();
$uuidProto->setString($uuid);
$proto->setUuid($uuidProto);
$proto->setName($name);
$existing = iterator_to_array($this->document->getMacroCollections());
$existing[] = $proto;
$this->document->setMacroCollections($existing);
$this->rebuildIndex();
return $this->getCollectionByUuid($uuid) ?? new MacroCollection($proto);
}
public function removeCollection(string $uuid): bool
{
$needle = strtoupper($uuid);
$kept = [];
$removed = false;
foreach ($this->document->getMacroCollections() as $proto) {
$current = strtoupper($proto->getUuid()?->getString() ?? '');
if (!$removed && $current === $needle) {
$removed = true;
continue;
}
$kept[] = $proto;
}
if (!$removed) {
return false;
}
$this->document->setMacroCollections($kept);
$this->rebuildIndex();
return true;
}
public function getDocument(): MacrosDocument public function getDocument(): MacrosDocument
{ {
return $this->document; return $this->document;
} }
private function rebuildIndex(): void
{
$this->macros = [];
$this->collections = [];
$this->macrosByUuid = [];
$this->macrosByName = [];
$this->collectionsByUuid = [];
$this->collectionsByName = [];
$this->collectionsByMacroUuid = [];
foreach ($this->document->getMacros() as $macroProto) {
$macro = new Macro($macroProto);
$this->macros[] = $macro;
$uuid = strtoupper($macro->getUuid());
if ($uuid !== '') {
$this->macrosByUuid[$uuid] = $macro;
}
$name = $macro->getName();
if ($name !== '') {
$this->macrosByName[$name] = $macro;
}
}
foreach ($this->document->getMacroCollections() as $collectionProto) {
$collection = new MacroCollection($collectionProto);
$this->collections[] = $collection;
$uuid = strtoupper($collection->getUuid());
if ($uuid !== '') {
$this->collectionsByUuid[$uuid] = $collection;
}
$name = $collection->getName();
if ($name !== '') {
$this->collectionsByName[$name] = $collection;
}
foreach ($collection->getMacroUuids() as $macroUuid) {
$key = strtoupper($macroUuid);
if ($key === '') {
continue;
}
$this->collectionsByMacroUuid[$key][] = $collection;
}
}
}
} }

26
src/MacrosFileWriter.php Normal file
View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use InvalidArgumentException;
use RuntimeException;
final class MacrosFileWriter
{
public static function write(MacroLibrary $library, string $filePath): void
{
$directory = dirname($filePath);
if (!is_dir($directory)) {
throw new InvalidArgumentException(sprintf('Target directory does not exist: %s', $directory));
}
$data = $library->getDocument()->serializeToString();
$writtenBytes = file_put_contents($filePath, $data);
if ($writtenBytes === false) {
throw new RuntimeException(sprintf('Unable to write Macros file: %s', $filePath));
}
}
}

103
src/Message.php Normal file
View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace ProPresenter\Parser;
use Rv\Data\Message as MessageProto;
use Rv\Data\UUID;
class Message
{
public function __construct(
private readonly MessageProto $message,
) {
}
public function getUuid(): string
{
return $this->message->getUuid()?->getString() ?? '';
}
public function setUuid(string $uuid): self
{
$proto = new UUID();
$proto->setString($uuid);
$this->message->setUuid($proto);
return $this;
}
public function getTitle(): string
{
return $this->message->getTitle();
}
public function setTitle(string $title): self
{
$this->message->setTitle($title);
return $this;
}
public function getTimeToRemove(): float
{
return $this->message->getTimeToRemove();
}
public function setTimeToRemove(float $timeToRemove): self
{
$this->message->setTimeToRemove($timeToRemove);
return $this;
}
public function isVisibleOnNetwork(): bool
{
return $this->message->getVisibleOnNetwork();
}
public function setVisibleOnNetwork(bool $visibleOnNetwork): self
{
$this->message->setVisibleOnNetwork($visibleOnNetwork);
return $this;
}
public function getMessageText(): string
{
return $this->message->getMessageText();
}
public function setMessageText(string $messageText): self
{
$this->message->setMessageText($messageText);
return $this;
}
public function getClearType(): int
{
return $this->message->getClearType();
}
public function setClearType(int $clearType): self
{
$this->message->setClearType($clearType);
return $this;
}
/**
* @return mixed raw repeated \Rv\Data\Message\Token protos
*/
public function getTokens(): mixed
{
return $this->message->getTokens();
}
public function getProto(): MessageProto
{
return $this->message;
}
}

Some files were not shown because too many files have changed in this diff Show more