propresenter-php/spec/pp_song_spec.md
2026-03-01 16:12:17 +01:00

19 KiB

ProPresenter 7 .pro File Format Specification

Version: 1.0
Target Audience: AI agents, automated parsers, developers
Proto Source: greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT License)


1. Overview

File Format

  • Extension: .pro
  • Binary Format: Protocol Buffers (Google protobuf v3)
  • Top-level Message: rv.data.Presentation (defined in presentation.proto)
  • Proto Definitions: greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT)

Known Limitations

  • Binary Fidelity: Round-trip decode→encode fails on all reference files. Proto definitions are incomplete; unknown fields are lost during serialization.
  • Workaround: Preserve original binary data if exact binary reproduction is required.

File Validity

  • Empty files (0 bytes): Invalid. Throw exception.
  • Songs without arrangements: Valid. 17 out of 169 reference files have no arrangements.
  • Non-song presentations: Files like ANKUENDIGUNGEN, MODERATION, THEMA have groups/slides but may lack text elements.

2. Song Structure

Hierarchy Diagram

Presentation (rv.data.Presentation)
├── name (string, field 1)
├── uuid (rv.data.UUID, field 5)
├── cue_groups[] (rv.data.Presentation.CueGroup, field 12) ← Groups
│   ├── group (rv.data.Group, field 1)
│   │   ├── name (string, field 2)
│   │   ├── uuid (rv.data.UUID, field 1)
│   │   └── color (rv.data.Color, field 3) [optional]
│   └── cue_identifiers[] (rv.data.UUID, field 2) ← Slide UUID references
├── cues[] (rv.data.Cue, field 13) ← Slides
│   ├── uuid (rv.data.UUID, field 1)
│   └── actions[0] (rv.data.Action, field 10)
│       └── slide (rv.data.Action.SlideType, field 23)
│           └── presentation (rv.data.PresentationSlide, field 2)
│               └── base_slide (rv.data.Slide, field 1)
│                   └── elements[] (rv.data.Slide.Element, field 1)
│                       └── element (rv.data.Graphics.Element, field 1)
│                           ├── name (string, field 2) ← Label like "Orginal", "Deutsch"
│                           └── text (rv.data.Graphics.Text, field 13)
│                               └── rtf_data (bytes, field 3) ← RTF-encoded text
└── arrangements[] (rv.data.Presentation.Arrangement, field 11)
    ├── name (string, field 2)
    ├── uuid (rv.data.UUID, field 1)
    └── group_identifiers[] (rv.data.UUID, field 3) ← Group UUID references

Navigation Paths

To access slide text:

Presentation
  → cues[i]
    → actions[0]
      → slide
        → presentation
          → base_slide
            → elements[j]
              → element
                → text.rtf_data

To access group metadata:

Presentation
  → cue_groups[i]
    → group
      → name, uuid, color

To access arrangement order:

Presentation
  → arrangements[i]
    → group_identifiers[]

3. Fields Reference

Presentation (rv.data.Presentation)

Field Path Protobuf Type Field Number Description
name string 1 Song title (e.g., "Amazing Grace")
uuid rv.data.UUID 5 Unique identifier for the presentation
cues[] rv.data.Cue 13 Array of slides
cue_groups[] rv.data.Presentation.CueGroup 12 Array of groups (song parts)
arrangements[] rv.data.Presentation.Arrangement 11 Array of arrangements

Presentation.CueGroup

Field Path Protobuf Type Field Number Description
group rv.data.Group 1 Group metadata (name, uuid, color)
cue_identifiers[] rv.data.UUID 2 Array of slide UUIDs in this group

Group (rv.data.Group)

Field Path Protobuf Type Field Number Description
uuid rv.data.UUID 1 Unique identifier for the group
name string 2 Display name (e.g., "Verse 1", "Chorus")
color rv.data.Color 3 Optional RGBA color (float values 0.0-1.0)

Presentation.Arrangement

Field Path Protobuf Type Field Number Description
uuid rv.data.UUID 1 Unique identifier for the arrangement
name string 2 Arrangement name (e.g., "normal", "test2")
group_identifiers[] rv.data.UUID 3 Ordered array of group UUIDs

Cue (rv.data.Cue)

Field Path Protobuf Type Field Number Description
uuid rv.data.UUID 1 Unique identifier for the slide
actions[] rv.data.Action 10 Array of actions (slides use actions[0])

Action (rv.data.Action)

Field Path Protobuf Type Field Number Description
slide rv.data.Action.SlideType 23 Slide data (oneof field)

Action.SlideType

Field Path Protobuf Type Field Number Description
presentation rv.data.PresentationSlide 2 Presentation slide (oneof field)

PresentationSlide (rv.data.PresentationSlide)

Field Path Protobuf Type Field Number Description
base_slide rv.data.Slide 1 Base slide containing elements

Slide (rv.data.Slide)

Field Path Protobuf Type Field Number Description
elements[] rv.data.Slide.Element 1 Array of slide elements

Slide.Element

Field Path Protobuf Type Field Number Description
element rv.data.Graphics.Element 1 Graphics element wrapper

Graphics.Element

Field Path Protobuf Type Field Number Description
uuid rv.data.UUID 1 Unique identifier for the element
name string 2 User-defined label (e.g., "Orginal", "Deutsch")
text rv.data.Graphics.Text 13 Text data (optional)

Graphics.Text

Field Path Protobuf Type Field Number Description
rtf_data bytes 3 RTF-encoded text content

4. Groups

Definition

Groups represent song parts (Verse 1, Verse 2, Chorus, Bridge, Ending, etc.). They define logical sections of a song.

Characteristics

  • Names: User-defined strings. Not standardized. Examples: "Verse 1", "Strophe 1", "Refrain", "Ending".
  • Slide References: Each group contains an ordered array of slide UUIDs (cue_identifiers).
  • Color: Optional RGBA color (float values 0.0-1.0 for red, green, blue, alpha).
  • Special Groups: COPYRIGHT, BLANK — treated as regular groups (no special handling required).

Example (Test.pro)

  • Verse 1 → 1 slide
  • Verse 2 → 1 slide
  • Chorus → 2 slides
  • Ending → 1 slide

Access Pattern

foreach ($presentation->getCueGroups() as $cueGroup) {
    $group = $cueGroup->getGroup();
    $name = $group->getName();
    $uuid = $group->getUuid()->getString();
    $slideUuids = [];
    foreach ($cueGroup->getCueIdentifiers() as $uuid) {
        $slideUuids[] = $uuid->getString();
    }
}

5. Slides

Definition

Slides are individual presentation frames. Each slide can contain multiple elements (text, shapes, media).

Navigation Path

Cue → actions[0] → slide → presentation → base_slide → elements[]

Text Elements

  • Location: base_slide.elements[] contains Slide.Element wrappers.
  • Graphics Element: Each Slide.Element wraps a Graphics.Element.
  • Text Data: Graphics.Element.text.rtf_data contains RTF-encoded text.
  • Element Name: Graphics.Element.name is a user-defined label (e.g., "Orginal", "Deutsch").

Slides Without Text

Some slides contain only media (images, videos) or shapes. These slides have elements[] with no text field set.

UUID References

Groups reference slides by UUID. Use Cue.uuid to match slides to group references.

Example (Test.pro)

  • 5 slides total
  • Chorus group → 2 slides (UUIDs referenced in cue_identifiers)

6. Arrangements

Definition

Arrangements define the order and selection of groups for a presentation. They specify which groups appear and in what sequence.

Characteristics

  • Group References: Ordered array of group UUIDs (group_identifiers).
  • Repetition: The same group UUID can appear multiple times (e.g., Chorus repeated 3 times).
  • Optional: Songs may have 0 or more arrangements.
  • No Arrangements: 17 out of 169 reference files have no arrangements. This is valid.

Example (Test.pro)

  • Arrangement "normal": Verse 1 → Chorus → Verse 2 → Chorus → Ending
  • Arrangement "test2": Verse 1 → Verse 2 → Chorus

Access Pattern

foreach ($presentation->getArrangements() as $arrangement) {
    $name = $arrangement->getName();
    $groupUuids = [];
    foreach ($arrangement->getGroupIdentifiers() as $uuid) {
        $groupUuids[] = $uuid->getString();
    }
}

7. Translations

Definition

Multiple elements[] per slide represent multiple text layers. The first element is the original text; subsequent elements are translations.

Characteristics

  • Element Count: 1 element = no translation. 2+ elements = translation present.
  • Element Names: User-defined labels (e.g., "Orginal", "Deutsch", "Text", "Text 2").
  • Label Patterns: 3 known patterns observed:
    1. "Orginal" / "Deutsch"
    2. "Text" / "Text 2"
    3. No specific naming (generic labels)
  • Not Standardized: Element names are arbitrary strings. Do NOT assume fixed labels.

Detection

$textElements = [];
foreach ($baseSlide->getElements() as $slideElement) {
    $graphicsElement = $slideElement->getElement();
    if ($graphicsElement !== null && $graphicsElement->hasText()) {
        $textElements[] = $graphicsElement;
    }
}

$hasTranslation = count($textElements) >= 2;
$originalText = $textElements[0]->getText()->getRtfData();
$translationText = $textElements[1]->getText()->getRtfData() ?? null;

Example (Test.pro)

  • Slide 1: 2 text elements → "Orginal" (German), "Deutsch" (English translation)
  • Element Names: User-defined, not standardized

8. Edge Cases

Empty Files

  • Size: 0 bytes
  • Validity: Invalid
  • Action: Throw exception

Songs Without Arrangements

  • Frequency: 17 out of 169 reference files
  • Validity: Valid
  • Behavior: arrangements[] is empty. Groups and slides still exist.

Non-Song Presentations

  • Examples: ANKUENDIGUNGEN, MODERATION, THEMA
  • Characteristics: Have groups and slides but may lack text elements.
  • Validity: Valid

Slides Without Text

  • Characteristics: elements[] contains shapes, media, or other non-text elements.
  • Detection: Graphics.Element.hasText() returns false.
  • Validity: Valid
  • Treatment: Regular groups (no special handling required).
  • Validity: Valid

9. RTF Text Format

Format Variant

  • Type: Apple CocoaRTF 2761
  • Encoding: Windows-1252 (ANSI codepage 1252)

Structure

{\rtf1\ansi\ansicpg1252\cocoartf2761
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0

\f0\fs96 \cf1 \CocoaLigature0 TEXT STARTS HERE}

Text Extraction

  • Text Start: After \CocoaLigature0 (space after 0 is the delimiter).
  • Soft Returns: \ + newline character = line break within slide.
  • Paragraph Breaks: \par = paragraph break.

Character Encoding

Windows-1252 Hex Escapes

  • Format: \'xx where xx is a hex byte value.
  • Examples:
    • \'fc → ü (U+00FC)
    • \'f6 → ö (U+00F6)
    • \'e4 → ä (U+00E4)
    • \'df → ß (U+00DF)

Unicode Escapes

  • Format: \uN? where N is a decimal codepoint, ? is an ANSI fallback character.
  • Examples:
    • \u8364? → € (U+20AC)
    • \u8220? → " (U+201C)
    • \u8221? → " (U+201D)
  • Negative Values: RTF uses signed 16-bit integers. Negative values are converted: codepoint + 65536.

Control Words

  • Format: \word[N] followed by space or non-alpha character.
  • Common Words:
    • \par → paragraph break
    • \CocoaLigature0 → text start marker
    • \f0, \fs96, \cf1 → formatting (font, size, color)
  • Delimiter: Space after control word is consumed (not part of text).

Escaped Characters

  • \{{
  • \}}
  • \\\ (or soft return in ProPresenter context)

Example RTF

{\rtf1\ansi\ansicpg1252\cocoartf2761
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\pard\tx560\pardirnatural\partightenfactor0

\f0\fs96 \cf1 \CocoaLigature0 Gro\'dfe Gnade\
Amazing Grace}

Plain Text Output:

Große Gnade
Amazing Grace

10. PHP Parser Usage

Installation

composer require propresenter/parser

Read a Song

use ProPresenter\Parser\ProFileReader;

$song = ProFileReader::read('path/to/song.pro');

Access Song Metadata

// Song name
$name = $song->getName();  // "Amazing Grace"

// Song UUID
$uuid = $song->getUuid();  // "A1B2C3D4-..."

// Groups
$groups = $song->getGroups();  // Group[]

// Slides
$slides = $song->getSlides();  // Slide[]

// Arrangements
$arrangements = $song->getArrangements();  // Arrangement[]

Access Groups

foreach ($song->getGroups() as $group) {
    $name = $group->getName();        // "Verse 1"
    $uuid = $group->getUuid();        // "E5F6G7H8-..."
    $color = $group->getColor();      // ['r' => 1.0, 'g' => 0.0, 'b' => 0.0, 'a' => 1.0] or null
    $slideUuids = $group->getSlideUuids();  // ["uuid1", "uuid2", ...]
}

Access Slides

foreach ($song->getSlides() as $slide) {
    $uuid = $slide->getUuid();
    $plainText = $slide->getPlainText();  // Extracted from first text element
    
    // Check for translation
    if ($slide->hasTranslation()) {
        $translation = $slide->getTranslation();
        $translatedText = $translation->getPlainText();
    }
    
    // Access all text elements
    foreach ($slide->getTextElements() as $textElement) {
        $name = $textElement->getName();        // "Orginal", "Deutsch", etc.
        $rtf = $textElement->getRtfData();      // Raw RTF bytes
        $plain = $textElement->getPlainText();  // Extracted plain text
    }
}

Access Arrangements

foreach ($song->getArrangements() as $arrangement) {
    $name = $arrangement->getName();  // "normal"
    $groupUuids = $arrangement->getGroupUuids();  // ["uuid1", "uuid2", "uuid1", ...]
    
    // Resolve groups
    $groups = $song->getGroupsForArrangement($arrangement);
    foreach ($groups as $group) {
        echo $group->getName() . "\n";
    }
}

Access Slides for a Group

$group = $song->getGroupByName("Chorus");
$slides = $song->getSlidesForGroup($group);

foreach ($slides as $slide) {
    echo $slide->getPlainText() . "\n";
}

Modify and Write

use ProPresenter\Parser\ProFileWriter;

// Modify song
$song->setName("New Song Title");

// Modify group
$group = $song->getGroupByName("Verse 1");
$group->setName("Strophe 1");

// Modify arrangement
$arrangement = $song->getArrangementByName("normal");
$arrangement->setName("default");

// Write to file
ProFileWriter::write($song, 'output.pro');

Error Handling

try {
    $song = ProFileReader::read('song.pro');
} catch (\RuntimeException $e) {
    // File not found, empty file, or invalid protobuf
    echo "Error: " . $e->getMessage();
}

Example: Extract All Text

$song = ProFileReader::read('song.pro');

foreach ($song->getGroups() as $group) {
    echo "Group: " . $group->getName() . "\n";
    
    $slides = $song->getSlidesForGroup($group);
    foreach ($slides as $slide) {
        echo "  Original: " . $slide->getPlainText() . "\n";
        
        if ($slide->hasTranslation()) {
            echo "  Translation: " . $slide->getTranslation()->getPlainText() . "\n";
        }
    }
}

Example: Create Arrangement

$song = ProFileReader::read('song.pro');

// Get group UUIDs
$verse1 = $song->getGroupByName("Verse 1");
$chorus = $song->getGroupByName("Chorus");
$verse2 = $song->getGroupByName("Verse 2");

// Create new arrangement
$arrangement = new Arrangement(new \Rv\Data\Presentation\Arrangement());
$arrangement->setName("custom");
$arrangement->setGroupUuids([
    $verse1->getUuid(),
    $chorus->getUuid(),
    $verse2->getUuid(),
    $chorus->getUuid(),
]);

// Add to song (requires direct protobuf access)
$song->getPresentation()->getArrangements()[] = $arrangement->getProto();

ProFileWriter::write($song, 'output.pro');

Appendix: Test.pro Structure

Groups (4)

  1. Verse 1 → 1 slide
  2. Verse 2 → 1 slide
  3. Chorus → 2 slides
  4. Ending → 1 slide

Slides (5)

  • Slide 1: Verse 1 text (2 text elements: "Orginal", "Deutsch")
  • Slide 2: Verse 2 text (2 text elements)
  • Slide 3: Chorus text part 1 (2 text elements)
  • Slide 4: Chorus text part 2 (2 text elements)
  • Slide 5: Ending text (2 text elements)

Arrangements (2)

  1. normal: Verse 1 → Chorus → Verse 2 → Chorus → Ending
  2. test2: Verse 1 → Verse 2 → Chorus

Appendix: Reference Statistics

  • Total Files: 169
  • Parseable Files: 168
  • Empty Files: 1 (invalid)
  • Files Without Arrangements: 17 (valid)
  • Binary Fidelity: 0 files pass round-trip decode→encode (proto definitions incomplete)

Appendix: Proto Field Numbers Quick Reference

Message Field Number
Presentation name 1
Presentation uuid 5
Presentation cues 13
Presentation cue_groups 12
Presentation arrangements 11
CueGroup group 1
CueGroup cue_identifiers 2
Group uuid 1
Group name 2
Group color 3
Arrangement uuid 1
Arrangement name 2
Arrangement group_identifiers 3
Cue uuid 1
Cue actions 10
Action slide 23
Action.SlideType presentation 2
PresentationSlide base_slide 1
Slide elements 1
Slide.Element element 1
Graphics.Element uuid 1
Graphics.Element name 2
Graphics.Element text 13
Graphics.Text rtf_data 3

End of Specification