propresenter-php/spec/pp_song_spec.md
Thorsten Bus f258f8f2ce [AI] update spec and docs with metadata and generator usage
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-01 16:34:17 +01:00

23 KiB

ProPresenter 7 .pro File Format Specification

Version: 1.1
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
application_info rv.data.ApplicationInfo 1 Platform and application version info
uuid rv.data.UUID 2 Unique identifier for the presentation
name string 3 Song title (e.g., "Amazing Grace")
last_date_used rv.data.Timestamp 4 Last date the song was used
last_modified_date rv.data.Timestamp 5 Last modification date
category string 6 Optional category label
notes string 7 Optional notes
background rv.data.Background 8 Background color/image
selected_arrangement rv.data.UUID 10 UUID of the currently selected arrangement
arrangements[] rv.data.Presentation.Arrangement 11 Array of arrangements
cue_groups[] rv.data.Presentation.CueGroup 12 Array of groups (song parts)
cues[] rv.data.Cue 13 Array of slides
ccli rv.data.Presentation.CCLI 14 CCLI licensing metadata
timeline rv.data.Presentation.Timeline 17 Timeline with duration
music_key string 22 Music key (rarely used)
music rv.data.Presentation.Music 23 Music key scale data

Presentation.CCLI

CCLI (Christian Copyright Licensing International) metadata. Present in 157 out of 168 reference files.

Field Path Protobuf Type Field Number Description
author string 1 Song author(s) (e.g., "Joel Houston, Matt Crocker")
artist_credits string 2 Artist credits (rarely used)
song_title string 3 CCLI song title
publisher string 4 Publisher (e.g., "2012 Hillsong Music Publishing")
copyright_year uint32 5 Copyright year (e.g., 2012)
song_number uint32 6 CCLI song number (e.g., 6428767)
display bool 7 Whether to display CCLI info
album string 8 Album name (rarely used)
artwork bytes 9 Album artwork (rarely used)

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 and UUID
$name = $song->getName();  // "Amazing Grace"
$uuid = $song->getUuid();  // "A1B2C3D4-..."

// CCLI metadata
$author = $song->getCcliAuthor();          // "Joel Houston, Matt Crocker"
$title = $song->getCcliSongTitle();        // "Oceans (Where Feet May Fail)"
$publisher = $song->getCcliPublisher();    // "2012 Hillsong Music Publishing"
$year = $song->getCcliCopyrightYear();     // 2012
$number = $song->getCcliSongNumber();      // 6428767
$display = $song->getCcliDisplay();        // true
$credits = $song->getCcliArtistCredits();  // ""
$album = $song->getCcliAlbum();            // ""

// Other metadata
$category = $song->getCategory();          // ""
$notes = $song->getNotes();                // ""
$selectedArr = $song->getSelectedArrangementUuid();  // "uuid-string"

// Groups, Slides, Arrangements
$groups = $song->getGroups();              // Group[]
$slides = $song->getSlides();              // Slide[]
$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 metadata
$song->setName("New Song Title");
$song->setCategory("Worship");
$song->setNotes("Use acoustic intro");

// Modify CCLI metadata
$song->setCcliAuthor("Author Name");
$song->setCcliSongTitle("Song Title");
$song->setCcliPublisher("Publisher");
$song->setCcliCopyrightYear(2024);
$song->setCcliSongNumber(12345);
$song->setCcliDisplay(true);

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

// 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');

Generate a New Song

use ProPresenter\Parser\ProFileGenerator;

$song = ProFileGenerator::generate(
    'Amazing Grace',
    [
        [
            'name' => 'Verse 1',
            'color' => [0.13, 0.59, 0.95, 1.0],
            'slides' => [
                ['text' => 'Amazing grace, how sweet the sound'],
                ['text' => 'That saved a wretch like me'],
            ],
        ],
        [
            'name' => 'Chorus',
            'color' => [0.95, 0.27, 0.27, 1.0],
            'slides' => [
                ['text' => 'I once was lost, but now am found'],
            ],
        ],
    ],
    [
        ['name' => 'normal', 'groupNames' => ['Verse 1', 'Chorus', 'Verse 1']],
    ],
    [
        'author' => 'John Newton',
        'song_title' => 'Amazing Grace',
        'copyright_year' => 1779,
    ],
);

// Write to file
ProFileGenerator::generateAndWrite('output.pro', 'Amazing Grace', $groups, $arrangements, $ccli);

Generate a Song with Translations

$song = ProFileGenerator::generate(
    'Oceans',
    [
        [
            'name' => 'Verse 1',
            'color' => [0.13, 0.59, 0.95, 1.0],
            'slides' => [
                [
                    'text' => 'You call me out upon the waters',
                    'translation' => 'Du rufst mich auf das Wasser',
                ],
            ],
        ],
    ],
    [
        ['name' => 'normal', 'groupNames' => ['Verse 1']],
    ],
);

Appendix: Test.pro Structure

Groups (4)

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

Slides (5)

  • Slides 1-2: Verse 1 text (2 text elements each: "Orginal", "Deutsch")
  • Slide 3: Verse 2 text (2 text elements)
  • Slide 4: Chorus text (2 text elements)
  • Slide 5: Ending text (2 text elements, with translations)

Arrangements (2)

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

Appendix: Reference Statistics

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

Appendix: Proto Field Numbers Quick Reference

Message Field Number
Presentation application_info 1
Presentation uuid 2
Presentation name 3
Presentation last_date_used 4
Presentation last_modified_date 5
Presentation category 6
Presentation notes 7
Presentation selected_arrangement 10
Presentation arrangements 11
Presentation cue_groups 12
Presentation cues 13
Presentation ccli 14
Presentation timeline 17
Presentation music_key 22
Presentation music 23
Presentation.CCLI author 1
Presentation.CCLI artist_credits 2
Presentation.CCLI song_title 3
Presentation.CCLI publisher 4
Presentation.CCLI copyright_year 5
Presentation.CCLI song_number 6
Presentation.CCLI display 7
Presentation.CCLI album 8
Presentation.CCLI artwork 9
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