feat(labels): add reader for global Labels file
Mirrors the Macros reader: LabelsFileReader -> LabelLibrary -> Label wraps the protobuf ProLabelsDocument and exposes each label's name (the proto 'text' field) plus optional RGBA color with a #RRGGBB hex helper. Includes CLI tool, PHPUnit suite against the bundled reference Labels sample, and api/labels.md docs.
This commit is contained in:
parent
b30918af41
commit
4e1ac9b7ea
|
|
@ -20,6 +20,7 @@ All project documentation lives in `doc/`. Load only what you need.
|
||||||
| 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 the global `Macros` file | `doc/api/macros.md` |
|
||||||
|
| Read the global `Labels` file | `doc/api/labels.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` |
|
||||||
|
|
@ -40,7 +41,9 @@ doc/
|
||||||
├── api/ ← PHP API docs (read/write/generate)
|
├── api/ ← PHP API docs (read/write/generate)
|
||||||
│ ├── song.md
|
│ ├── song.md
|
||||||
│ ├── playlist.md
|
│ ├── playlist.md
|
||||||
│ └── bundle.md
|
│ ├── bundle.md
|
||||||
|
│ ├── macros.md
|
||||||
|
│ └── labels.md
|
||||||
└── internal/ ← Dev notes (learnings, decisions, issues)
|
└── internal/ ← Dev notes (learnings, decisions, issues)
|
||||||
├── learnings.md
|
├── learnings.md
|
||||||
├── decisions.md
|
├── decisions.md
|
||||||
|
|
@ -55,6 +58,7 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files:
|
||||||
- **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
|
- **Macros** (`Macros`, no extension) — Global protobuf-encoded macro library with collections
|
||||||
|
- **Labels** (`Labels`, no extension) — Global protobuf-encoded label library (slide labels with optional UI colors)
|
||||||
|
|
||||||
### CLI Tools
|
### CLI Tools
|
||||||
|
|
||||||
|
|
@ -62,6 +66,7 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files:
|
||||||
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
|
||||||
php bin/parse-macros.php path/to/Macros
|
php bin/parse-macros.php path/to/Macros
|
||||||
|
php bin/parse-labels.php path/to/Labels
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Source Files
|
### Key Source Files
|
||||||
|
|
|
||||||
47
bin/parse-labels.php
Executable file
47
bin/parse-labels.php
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use ProPresenter\Parser\LabelsFileReader;
|
||||||
|
|
||||||
|
if ($argc < 2) {
|
||||||
|
echo "Usage: parse-labels.php <Labels>\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $argv[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$library = LabelsFileReader::read($filePath);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = $library->getLabels();
|
||||||
|
|
||||||
|
echo "Labels (" . count($labels) . "):\n";
|
||||||
|
foreach ($labels as $index => $label) {
|
||||||
|
$number = $index + 1;
|
||||||
|
$name = $label->getName();
|
||||||
|
$displayName = $name === '' ? '(unnamed)' : $name;
|
||||||
|
|
||||||
|
if ($label->hasColor()) {
|
||||||
|
$color = $label->getColor();
|
||||||
|
$colorPart = sprintf(
|
||||||
|
'%s rgba(%.3f, %.3f, %.3f, %.3f)',
|
||||||
|
$label->getColorHex(),
|
||||||
|
$color['r'],
|
||||||
|
$color['g'],
|
||||||
|
$color['b'],
|
||||||
|
$color['a'],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$colorPart = '(no color)';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo sprintf(" [%d] %s :: %s\n", $number, $displayName, $colorPart);
|
||||||
|
}
|
||||||
10
doc/INDEX.md
10
doc/INDEX.md
|
|
@ -10,6 +10,7 @@
|
||||||
| 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 the global `Macros` file | [api/macros.md](api/macros.md) |
|
||||||
|
| Read the global `Labels` file | [api/labels.md](api/labels.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) |
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
- [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/macros.md](api/macros.md) — Macros library API (read the global `Macros` file)
|
||||||
|
- [api/labels.md](api/labels.md) — Labels library API (read the global `Labels` file)
|
||||||
|
|
||||||
### 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
|
||||||
|
|
@ -57,7 +59,8 @@ doc/
|
||||||
│ ├── song.md
|
│ ├── song.md
|
||||||
│ ├── playlist.md
|
│ ├── playlist.md
|
||||||
│ ├── bundle.md
|
│ ├── bundle.md
|
||||||
│ └── macros.md
|
│ ├── macros.md
|
||||||
|
│ └── labels.md
|
||||||
└── internal/ ← Development notes (optional context)
|
└── internal/ ← Development notes (optional context)
|
||||||
├── learnings.md
|
├── learnings.md
|
||||||
├── decisions.md
|
├── decisions.md
|
||||||
|
|
@ -128,6 +131,8 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
|
||||||
| `src/ProBundleWriter.php` | Write `.probundle` files |
|
| `src/ProBundleWriter.php` | Write `.probundle` files |
|
||||||
| `src/MacroLibrary.php` | Macros library wrapper (read global `Macros` file) |
|
| `src/MacroLibrary.php` | Macros library wrapper (read global `Macros` file) |
|
||||||
| `src/MacrosFileReader.php` | Read global `Macros` file |
|
| `src/MacrosFileReader.php` | Read global `Macros` file |
|
||||||
|
| `src/LabelLibrary.php` | Labels library wrapper (read global `Labels` file) |
|
||||||
|
| `src/LabelsFileReader.php` | Read global `Labels` file |
|
||||||
|
|
||||||
### CLI Tools
|
### CLI Tools
|
||||||
|
|
||||||
|
|
@ -140,4 +145,7 @@ php bin/parse-playlist.php path/to/playlist.proplaylist
|
||||||
|
|
||||||
# Parse and display the global Macros file
|
# Parse and display the global Macros file
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
137
doc/api/labels.md
Normal file
137
doc/api/labels.md
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# Labels Library API
|
||||||
|
|
||||||
|
> PHP module for reading the global ProPresenter `Labels` file (raw protobuf,
|
||||||
|
> no extension) and exposing each label's name and UI color.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ProPresenter\Parser\LabelsFileReader;
|
||||||
|
|
||||||
|
$library = LabelsFileReader::read('/path/to/Labels');
|
||||||
|
|
||||||
|
foreach ($library->getLabels() as $label) {
|
||||||
|
$label->getName(); // "KeyVisual Beamer"
|
||||||
|
$label->hasColor(); // bool
|
||||||
|
$label->getColor(); // ['r'=>0.0,'g'=>0.408,'b'=>0.702,'a'=>1.0] | null
|
||||||
|
$label->getColorHex(); // "#0068B3" | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
The `Labels` file is the protobuf-serialised
|
||||||
|
[`ProLabelsDocument`](../../proto/labels.proto):
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `labels` | repeated `Action.Label` | Definitions: text + optional color |
|
||||||
|
|
||||||
|
Each `Action.Label` carries:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `text` | string | Display name (exposed as `getName()` on the wrapper) |
|
||||||
|
| `color` | `Color` (optional) | RGBA float channels in 0..1; absent for system / "no color" labels |
|
||||||
|
|
||||||
|
Labels are identified by name only — there is no UUID. Slides reference
|
||||||
|
labels by name from inside `.pro` files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reading
|
||||||
|
|
||||||
|
```php
|
||||||
|
use ProPresenter\Parser\LabelsFileReader;
|
||||||
|
|
||||||
|
$library = LabelsFileReader::read('/Users/me/.../Labels');
|
||||||
|
```
|
||||||
|
|
||||||
|
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
|
||||||
|
empty / unreadable files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LabelLibrary
|
||||||
|
|
||||||
|
Top-level wrapper around `Rv\Data\ProLabelsDocument`. Indexes labels by name
|
||||||
|
for fast lookup.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$library->getLabels(); // Label[]
|
||||||
|
$library->count(); // int
|
||||||
|
$library->getLabelByName('Szene 1'); // ?Label (case-sensitive)
|
||||||
|
$library->findLabelByName('szene 1'); // ?Label (case-insensitive)
|
||||||
|
$library->getDocument(); // \Rv\Data\ProLabelsDocument
|
||||||
|
```
|
||||||
|
|
||||||
|
If the same name appears more than once in the source document the first
|
||||||
|
occurrence wins for both lookup helpers; every entry is preserved in
|
||||||
|
`getLabels()` in document order.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Label
|
||||||
|
|
||||||
|
```php
|
||||||
|
$label->getName(); // "KeyVisual Beamer" (proto field is `text`)
|
||||||
|
$label->hasColor(); // bool — was a Color message present?
|
||||||
|
$label->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
|
||||||
|
$label->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null
|
||||||
|
$label->getProto(); // \Rv\Data\Action\Label (raw protobuf)
|
||||||
|
```
|
||||||
|
|
||||||
|
Color channels are floats in 0..1 as ProPresenter stores them. `getColorHex()`
|
||||||
|
clamps and rounds each channel to 8 bits before formatting.
|
||||||
|
|
||||||
|
A label can legitimately exist without a `color` message. Treat that as
|
||||||
|
"use the default UI color", not as black. The reference sample's first four
|
||||||
|
labels (`Leere Folie`, `Instrumental`, `Wiederholen`, `Gesprochenes Wort`)
|
||||||
|
hit this case.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Tool
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/parse-labels.php /path/to/Labels
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Labels (15):
|
||||||
|
[1] Leere Folie :: (no color)
|
||||||
|
[2] Instrumental :: (no color)
|
||||||
|
[3] Wiederholen :: (no color)
|
||||||
|
[4] Gesprochenes Wort :: (no color)
|
||||||
|
[5] KeyVisual Stream & Beamer mit Countdown :: #CC298B rgba(0.800, 0.161, 0.545, 1.000)
|
||||||
|
[6] KeyVisual Stream & Beamer mit Jingle :: #7600CC rgba(0.463, 0.000, 0.800, 1.000)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `src/LabelLibrary.php` | Document-level wrapper with name lookups |
|
||||||
|
| `src/Label.php` | Single label wrapper (name, color, hex) |
|
||||||
|
| `src/LabelsFileReader.php` | Reads the `Labels` file |
|
||||||
|
| `bin/parse-labels.php` | CLI tool |
|
||||||
|
| `proto/labels.proto` | Protobuf schema (just imports `Action.Label`) |
|
||||||
|
| `proto/action.proto` | Defines the inner `Action.Label` message |
|
||||||
|
| `generated/Rv/Data/ProLabelsDocument.php` | Generated message class |
|
||||||
|
| `generated/Rv/Data/Action/Label.php` | Generated label message class |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope Notes
|
||||||
|
|
||||||
|
This module is read-only by design. Writing the `Labels` file back, editing
|
||||||
|
slide-side label references on `.pro` files, or syncing labels across devices
|
||||||
|
are not implemented here. Add them by mirroring the `ProFileWriter` /
|
||||||
|
`ProFileGenerator` pattern when needed.
|
||||||
|
|
@ -74,16 +74,20 @@
|
||||||
| 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 |
|
||||||
| 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 |
|
||||||
| label | [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) |
|
||||||
|
| LabelLibrary | [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) |
|
||||||
|
|
||||||
## 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) |
|
| 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) |
|
||||||
| 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) |
|
||||||
| 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) |
|
| 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) |
|
||||||
| MacrosFileReader | [api/macros.md](api/macros.md) |
|
| MacrosFileReader | [api/macros.md](api/macros.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) |
|
||||||
|
|
@ -96,8 +100,8 @@
|
||||||
| PresentationBundle | [api/bundle.md](api/bundle.md) |
|
| PresentationBundle | [api/bundle.md](api/bundle.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) |
|
| 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) |
|
||||||
| command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.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
|
||||||
|
|
||||||
|
|
|
||||||
BIN
doc/reference_samples/Labels
Normal file
BIN
doc/reference_samples/Labels
Normal file
Binary file not shown.
91
src/Label.php
Normal file
91
src/Label.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ProPresenter\Parser;
|
||||||
|
|
||||||
|
use Rv\Data\Action\Label as LabelProto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LabelProto $label,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display name (the protobuf `text` field; this is what the
|
||||||
|
* ProPresenter UI renders next to the swatch).
|
||||||
|
*/
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->label->getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the label carries an explicit color message (the absence of a
|
||||||
|
* color is not the same as black: ProPresenter renders unset labels with
|
||||||
|
* the default UI color).
|
||||||
|
*/
|
||||||
|
public function hasColor(): bool
|
||||||
|
{
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public function getColor(): ?array
|
||||||
|
{
|
||||||
|
if (!$this->label->hasColor()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$color = $this->label->getColor();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'r' => $color->getRed(),
|
||||||
|
'g' => $color->getGreen(),
|
||||||
|
'b' => $color->getBlue(),
|
||||||
|
'a' => $color->getAlpha(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
$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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying protobuf Label message.
|
||||||
|
*/
|
||||||
|
public function getProto(): LabelProto
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/LabelLibrary.php
Normal file
83
src/LabelLibrary.php
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ProPresenter\Parser;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/** @var Label[] */
|
||||||
|
private array $labels = [];
|
||||||
|
|
||||||
|
/** @var array<string, Label> */
|
||||||
|
private array $labelsByName = [];
|
||||||
|
|
||||||
|
/** @var array<string, Label> */
|
||||||
|
private array $labelsByNameLower = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProLabelsDocument $document,
|
||||||
|
) {
|
||||||
|
foreach ($this->document->getLabels() as $labelProto) {
|
||||||
|
$label = new Label($labelProto);
|
||||||
|
$this->labels[] = $label;
|
||||||
|
|
||||||
|
$name = $label->getName();
|
||||||
|
if ($name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->labelsByName[$name] ??= $label;
|
||||||
|
$this->labelsByNameLower[strtolower($name)] ??= $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Label[]
|
||||||
|
*/
|
||||||
|
public function getLabels(): array
|
||||||
|
{
|
||||||
|
return $this->labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exact-name lookup. Use {@see findLabelByName()} for a
|
||||||
|
* case-insensitive variant.
|
||||||
|
*/
|
||||||
|
public function getLabelByName(string $name): ?Label
|
||||||
|
{
|
||||||
|
return $this->labelsByName[$name] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-insensitive name lookup.
|
||||||
|
*/
|
||||||
|
public function findLabelByName(string $name): ?Label
|
||||||
|
{
|
||||||
|
return $this->labelsByNameLower[strtolower($name)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the underlying protobuf ProLabelsDocument.
|
||||||
|
*/
|
||||||
|
public function getDocument(): ProLabelsDocument
|
||||||
|
{
|
||||||
|
return $this->document;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/LabelsFileReader.php
Normal file
42
src/LabelsFileReader.php
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ProPresenter\Parser;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Rv\Data\ProLabelsDocument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader for the global ProPresenter `Labels` file (a raw protobuf
|
||||||
|
* serialisation of {@see ProLabelsDocument}, no extension).
|
||||||
|
*/
|
||||||
|
final class LabelsFileReader
|
||||||
|
{
|
||||||
|
public static function read(string $filePath): LabelLibrary
|
||||||
|
{
|
||||||
|
if ($filePath === '' || !is_file($filePath)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Labels 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('Labels file is empty: %s', $filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = file_get_contents($filePath);
|
||||||
|
if ($data === false) {
|
||||||
|
throw new RuntimeException(sprintf('Unable to read labels file: %s', $filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new ProLabelsDocument();
|
||||||
|
$document->mergeFromString($data);
|
||||||
|
|
||||||
|
return new LabelLibrary($document);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
tests/LabelsFileReaderTest.php
Normal file
136
tests/LabelsFileReaderTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ProPresenter\Parser\Tests;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ProPresenter\Parser\Label;
|
||||||
|
use ProPresenter\Parser\LabelLibrary;
|
||||||
|
use ProPresenter\Parser\LabelsFileReader;
|
||||||
|
|
||||||
|
class LabelsFileReaderTest extends TestCase
|
||||||
|
{
|
||||||
|
private const REFERENCE_PATH = __DIR__ . '/../doc/reference_samples/Labels';
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function readThrowsOnMissingFile(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
LabelsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-labels');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function readReturnsLabelLibraryWithExpectedCount(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(LabelLibrary::class, $library);
|
||||||
|
$this->assertCount(15, $library->getLabels());
|
||||||
|
$this->assertSame(15, $library->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function labelsExposeNames(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
$labels = $library->getLabels();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Label::class, $labels[0]);
|
||||||
|
$this->assertSame('Leere Folie', $labels[0]->getName());
|
||||||
|
$this->assertSame('Instrumental', $labels[1]->getName());
|
||||||
|
$this->assertSame('KeyVisual Stream & Beamer mit Countdown', $labels[4]->getName());
|
||||||
|
$this->assertSame('Szene 3', $labels[14]->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function labelsWithoutColorReportNullColor(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$leereFolie = $library->getLabelByName('Leere Folie');
|
||||||
|
$this->assertNotNull($leereFolie);
|
||||||
|
$this->assertFalse($leereFolie->hasColor());
|
||||||
|
$this->assertNull($leereFolie->getColor());
|
||||||
|
$this->assertNull($leereFolie->getColorHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function labelsWithColorReturnFloatChannels(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$beamer = $library->getLabelByName('KeyVisual Beamer');
|
||||||
|
$this->assertNotNull($beamer);
|
||||||
|
$this->assertTrue($beamer->hasColor());
|
||||||
|
|
||||||
|
$color = $beamer->getColor();
|
||||||
|
$this->assertIsArray($color);
|
||||||
|
$this->assertEqualsWithDelta(0.0, $color['r'], 0.001);
|
||||||
|
$this->assertEqualsWithDelta(0.4078, $color['g'], 0.001);
|
||||||
|
$this->assertEqualsWithDelta(0.7020, $color['b'], 0.001);
|
||||||
|
$this->assertEqualsWithDelta(1.0, $color['a'], 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function colorHexIsSixDigitUppercase(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$beamer = $library->getLabelByName('KeyVisual Beamer');
|
||||||
|
$this->assertNotNull($beamer);
|
||||||
|
$this->assertSame('#0068B3', $beamer->getColorHex());
|
||||||
|
|
||||||
|
$countdown = $library->getLabelByName('KeyVisual Stream & Beamer mit Countdown');
|
||||||
|
$this->assertNotNull($countdown);
|
||||||
|
$this->assertSame('#CC298B', $countdown->getColorHex());
|
||||||
|
|
||||||
|
$kill = $library->getLabelByName('Namenseinblender Kill');
|
||||||
|
$this->assertNotNull($kill);
|
||||||
|
$this->assertSame('#000000', $kill->getColorHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function getLabelByNameIsCaseSensitive(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$this->assertNotNull($library->getLabelByName('Szene 1'));
|
||||||
|
$this->assertNull($library->getLabelByName('szene 1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function findLabelByNameIsCaseInsensitive(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$exact = $library->findLabelByName('Szene 1');
|
||||||
|
$lower = $library->findLabelByName('szene 1');
|
||||||
|
$upper = $library->findLabelByName('SZENE 1');
|
||||||
|
|
||||||
|
$this->assertNotNull($exact);
|
||||||
|
$this->assertSame($exact, $lower);
|
||||||
|
$this->assertSame($exact, $upper);
|
||||||
|
$this->assertSame('Szene 1', $exact->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function unknownNameReturnsNull(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$this->assertNull($library->getLabelByName('does-not-exist'));
|
||||||
|
$this->assertNull($library->findLabelByName('does-not-exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function documentIsExposedForRawAccess(): void
|
||||||
|
{
|
||||||
|
$library = LabelsFileReader::read(self::REFERENCE_PATH);
|
||||||
|
|
||||||
|
$document = $library->getDocument();
|
||||||
|
$this->assertCount(15, $document->getLabels());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue