PHP Library to generate and read propresenter files (.pro, .probundle, .proplaylist)
Find a file
Thorsten Bus 2e3ed500fe fix(rtf): escape all non-ASCII text for \ansicpg1252 bodies
The RTF body of every generated slide declares \ansicpg1252, but the
encoder only escaped seven German umlauts and wrote every other
non-ASCII character as raw UTF-8 bytes. ProPresenter reads those bytes
as Windows-1252, so U+2019 (UTF-8 E2 80 99) rendered as "’" —
"I’m" became "I’m".

Add RtfEncoder as the single encoding authority and use it from both
ProFileGenerator and TextElement (which carried a duplicate of the same
broken table):

- Latin-1 range and the Windows-1252 specials (’ – — … „ “ € ™) are
  emitted as \'xx hex escapes.
- Everything else uses \uNNNN? unicode escapes, with characters above
  the BMP (emoji) written as a UTF-16 surrogate pair.
- RTF structural characters \ { } are escaped, newlines stay soft
  returns, and invalid UTF-8 input is repaired instead of passed on.

The reader could not read surrogate pairs back: mb_chr() returns false
for a lone surrogate, which tripped the string return type. RtfExtractor
now recombines the pair and drops unpaired surrogates.

Covered by RtfEncoderTest (encoding table, ASCII-only guarantee) and
RtfUnicodeRoundTripTest (generate -> write .pro -> read -> compare).
2026-08-23 14:54:03 +02:00
bin Initial public release 2026-05-03 22:21:01 +02:00
doc Upgrade proto schema to ProPresenter Proto 19beta 2026-05-04 07:19:24 +02:00
generated Upgrade proto schema to ProPresenter Proto 19beta 2026-05-04 07:19:24 +02:00
proto Upgrade proto schema to ProPresenter Proto 19beta 2026-05-04 07:19:24 +02:00
src fix(rtf): escape all non-ASCII text for \ansicpg1252 bodies 2026-08-23 14:54:03 +02:00
tests fix(rtf): escape all non-ASCII text for \ansicpg1252 bodies 2026-08-23 14:54:03 +02:00
.gitignore Initial public release 2026-05-03 22:21:01 +02:00
AGENTS.md Initial public release 2026-05-03 22:21:01 +02:00
composer.json Rename package to bussnet/propresenter7-php-lib 2026-05-04 07:19:00 +02:00
composer.lock Initial public release 2026-05-03 22:21:01 +02:00
LICENSE Upgrade proto schema to ProPresenter Proto 19beta 2026-05-04 07:19:24 +02:00
phpunit.xml Initial public release 2026-05-03 22:21:01 +02:00
README.md feat(generator): add text color to slide text elements 2026-08-23 14:16:27 +02:00

ProPresenter 7 PHP Library

A PHP library to read, modify, and generate ProPresenter 7 files — songs, playlists, bundles, themes, and global library files.

PHP Version License: MIT Tests Built on Protocol Buffers

ProPresenter 7 stores its data in protobuf-encoded binary files (with ZIP wrappers for playlists and bundles). This library decodes those formats into idiomatic PHP objects, lets you modify them, and writes them back out — with full round-trip fidelity for global library files and verified compatibility with PP7 for songs and bundles.


Table of Contents


Features

File formats supported

Format Extension Read Modify Generate Notes
Song .pro Lyrics, groups, slides, arrangements, translations, CCLI metadata, macros, media
Playlist .proplaylist ZIP64 archive, embedded songs, headers, placeholders
Bundle .probundle ZIP archive containing a song + flat media assets
Theme folder Theme protobuf + Assets/ directory
Macros Macros Macros + collections
Labels Labels Slide labels with optional UI colors
Groups Groups Library groups (UUID, color, hot keys)
ClearGroups ClearGroups Clear-action groups
CCLI CCLI License, copyright template
Messages Messages Lower-third / overlay messages
Timers Timers Timer definitions + clock format
Stage Stage Stage display layouts
Workspace Workspace Screens, looks, masks, audio/video inputs
Props Props Prop cues + transitions
TestPatterns TestPatterns Test pattern overrides
Calendar Calendar Scheduled events firing macros
KeyMappings KeyMappings Custom hot-key bindings
CommunicationDevices JSON MIDI / serial / OSC bindings

Highlights

  • High-level wrappers — work with Song, Group, Slide, Arrangement, PlaylistArchive etc. instead of raw protobuf classes.
  • RTF text extractionSlide::getPlainText() returns clean text from ProPresenter's CocoaRTF, including German umlauts and Unicode.
  • Translation-aware — read and write multi-language slides (hasTranslation(), getTranslation()).
  • ZIP64 repair — automatically fixes ProPresenter's 98-byte ZIP64 header bug on read.
  • Generate from scratch — build complete .pro and .proplaylist files programmatically with media references.
  • 18 CLI tools — quickly inspect any ProPresenter file from the command line.
  • 369 tests, 1,300+ assertions — covering all readers, writers, generators, and round-trip fidelity against a synthetic test corpus.
  • Comprehensive docs — every API and binary format is documented in doc/.

Requirements

  • PHP 8.4 or higher
  • google/protobuf (installed via Composer)
  • ext-zip for .proplaylist and .probundle files (bundled with most PHP distributions)

Installation

composer require bussnet/propresenter7-php-lib

Or clone the repository to develop locally:

git clone https://github.com/bussnet/propresenter7-php-lib.git
cd propresenter7-php-lib
composer install

Getting Started

All examples assume Composer's autoloader is loaded:

require 'vendor/autoload.php';

1. Read a song (.pro)

use ProPresenter\Parser\ProFileReader;

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

echo $song->getName() . "\n";              // "Amazing Grace"
echo $song->getCcliAuthor() . "\n";        // "John Newton"
echo $song->getCcliCopyrightYear() . "\n"; // 1779

// Walk groups → slides → text
foreach ($song->getGroups() as $group) {
    echo "[{$group->getName()}]\n";

    foreach ($song->getSlidesForGroup($group) as $slide) {
        echo "  " . $slide->getPlainText() . "\n";

        if ($slide->hasTranslation()) {
            echo "  → " . $slide->getTranslation()->getPlainText() . "\n";
        }
    }
}

// Resolve an arrangement to a flat list of groups (in performance order)
$arrangement = $song->getArrangements()[0];
foreach ($song->getGroupsForArrangement($arrangement) as $group) {
    echo $group->getName() . " → ";
}

2. Modify and save a song

use ProPresenter\Parser\ProFileReader;
use ProPresenter\Parser\ProFileWriter;

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

// Update CCLI metadata
$song->setName('Amazing Grace (My Chains Are Gone)');
$song->setCcliPublisher('Public Domain');
$song->setCcliCopyrightYear(2006);

// Rename a group
$song->getGroupByName('Verse 1')?->setName('Strophe 1');

// Add a label to the first slide
$song->getSlides()[0]->setLabel('Intro');

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

3. Generate a song from scratch

use ProPresenter\Parser\ProFileGenerator;

ProFileGenerator::generateAndWrite(
    'amazing-grace.pro',
    'Amazing Grace',
    [
        [
            'name'  => 'Verse 1',
            'color' => [0.13, 0.59, 0.95, 1.0], // RGBA floats (0..1)
            'slides' => [
                ['text' => "Amazing grace, how sweet the sound\nThat saved a wretch like me"],
                [
                    'text'        => 'I once was lost, but now am found',
                    'translation' => 'Ich war verloren, doch jetzt gefunden',
                ],
            ],
        ],
        [
            'name'  => 'Chorus',
            'color' => [0.95, 0.27, 0.27, 1.0],
            'slides' => [
                ['text' => 'My chains are gone, I have been set free'],
            ],
        ],
    ],
    [
        ['name' => 'normal', 'groupNames' => ['Verse 1', 'Chorus', 'Verse 1', 'Chorus']],
    ],
    [
        'author'         => 'John Newton',
        'song_title'     => 'Amazing Grace',
        'copyright_year' => 1779,
    ],
);

Supported slideData keys

Every entry of a group's slides array is a slideData array. All keys are optional.

Key Type Description
text string Main slide text (multi-line allowed).
translation string Second text element; renders original + translation side by side.
subtitle string Smaller non-bold second run below text (ignored when translation is set).
textBounds array Explicit placement of the plain text element (see below).
textStyle array Explicit alignment of the plain text element (see below).
imageOnly bool Skip the text layer entirely (image-only slide).
media string Foreground media filename.
background array Background media layer (a media ACTION), e.g. ['path' => 'BACKGROUND.jpg', 'bundleRelative' => true].
image array Image content ELEMENT appended LAST, i.e. the backmost layer of the slide, behind text (see below).
label string Slide label text.
clock array Live wall-clock element (see below).
timer array Timer/countdown element bound to a ProPresenter timer (see below).
textBounds / textStyle keys

By default the plain text element covers the historic text-safe area (x:150, y:100, width:1620, height:880) and is centred both horizontally and vertically. textBounds and textStyle override that per slide — useful to place a short line (e.g. a name tag) in one of the slide's corners.

Key Type Default Description
textBounds.x float 150 Left edge in slide coordinates.
textBounds.y float 100 Top edge in slide coordinates.
textBounds.width float 1620 Box width.
textBounds.height float 880 Box height.
textStyle.align string 'center' left, center or right.
textStyle.verticalAlign string 'middle' top, middle or bottom.
textStyle.color array white [r, g, b] as 0..255 ints or 0..1 floats.

Missing sub-keys fall back to their default, so a partial textBounds is valid. Omitting both keys keeps the generated element byte-identical to previously generated files. The same holds for textStyle.color: without it the RTF colour table keeps its historic all-white entries.

// Name tag pinned to the bottom-left corner
[
    'text'       => 'Max Mustermann',
    'subtitle'   => 'Moderation',
    'textBounds' => ['x' => 60, 'y' => 820, 'width' => 600, 'height' => 200],
    'textStyle'  => ['align' => 'left', 'verticalAlign' => 'bottom'],
]

// Amber name tag
[
    'text'      => 'Max Mustermann',
    'textStyle' => ['color' => [255, 200, 0]],
]

Slides read back from a .pro file expose getTextElementBounds(), getTextElementAlign() and getTextElementVerticalAlign(), which resolve the first plain text element (skipping clock, timer and image elements).

The colour is read back with Slide::getTextColor() (first plain text element) and Slide::getTimerColor() (timer element), both returning an [r, g, b] triple with 0..255 components, or null when the slide carries no such element. TextElement::getTextColor() exposes the same value per element. All three parse the second colour table entry — the one the RTF body references via \cf2.

image keys

Unlike background — which emits a media action on the background layer — image emits a real slide content element whose fill is the given image.

ProPresenter paints a slide's element stack front-to-back: the lowest index is the frontmost layer, the highest index is the backmost layer. The image element is therefore appended LAST to the slide's element array, i.e. it is the backmost layer, so text, translation, subtitle, clock and timer elements — all emitted before it — are painted on top of it. Combine image with imageOnly => true for an image-only slide, or with text for text over an image.

The image is referenced bundle-relative by its bare filename (path is reduced to basename()), so it resolves against the bytes embedded in the .pro / .probundle archive — never by an absolute path.

Key Type Default Description
path string '' Bare filename, referenced bundle-relative.
format string 'JPG' Media format, e.g. JPG, PNG.
width int 1920 Natural image width.
height int 1080 Natural image height.
bounds array x:0, y:0, width:1920, height:1080 ['x','y','width','height'] in slide coordinates.
scaleBehavior string 'fill' fill, fit or stretch.
opacity float 1.0 Element opacity.
name string '' Name of the graphics element.
// Uploaded info slide image with text rendered on top of it
['text' => 'Herzlich willkommen', 'image' => ['path' => 'INFO_1.jpg', 'format' => 'JPG']]

// Image-only slide (no text layer)
['imageOnly' => true, 'image' => ['path' => 'INFO_2.jpg']]

Slides read back from a .pro file expose hasImageElement(), getImageElementUrl() and getImageElementFormat() (mirroring hasBackgroundMedia() / getBackgroundMediaUrl() / getBackgroundMediaFormat() for the background media action).

clock keys
Key Type Default Description
format string 'HH:mm' Clock format; drives Clock.Format, never written verbatim (see note below).
military24 bool true 24-hour time.
text string derived from format Static placeholder text shown in the editor.
bounds array x:60, y:40, width:600, height:200 ['x','y','width','height'] in slide coordinates.
style array Text styling, see below.
timer keys
Key Type Default Description
timerUuid string UUID of the timer in the ProPresenter Timers library. Omit to leave unbound.
timerName string '' Timer name (fallback lookup when the UUID is unknown).
format string 'mm:ss' Format string; alias formatString. Components present in the string (H/h, m, s, S) are shown (Style LONG), the rest hidden (Style NONE). Drives Timer.Format, never written verbatim (see note below).
text string derived from format Static placeholder text shown in the editor.
name string 'Timer' Name of the graphics element.
bounds array x:60, y:40, width:1800, height:1000 ['x','y','width','height'] in slide coordinates.
style array Text styling, see below.
military24 bool false Maps to Timer.Format.is_24_hour_time.
wallClock bool false Maps to Timer.Format.is_wall_clock_time.
millisecondsUnderMinuteOnly bool false Maps to Timer.Format.show_milliseconds_under_minute_only.
visibleWhen string Optional visibility condition bound to the same timer: hasTimeRemaining, hasExpired, isRunning or notRunning. Emitted as an additional VisibilityLink DataLink so ProPresenter hides the element once the condition no longer holds. Omit to keep the element always visible.
style keys (shared by clock and timer)
Key Type Default Description
fontName string 'HelveticaNeue' Font family.
fontSize int 42 Font size in points.
bold bool false Bold text run.
color array white [r, g, b] as 0..255 ints or 0..1 floats.
align string 'center' left, center or right.
verticalAlign string 'middle' top, middle or bottom.

Omitting style keeps the default RTF template byte-identical to previously generated files.

// Big centred countdown bound to a timer from the Timers library
['timer' => [
    'timerUuid' => '0E45D0AF-BCC2-4A31-BCFD-0F5A3358E225',
    'timerName' => 'Gottesdienst',
    'format'    => 'mm:ss',
    'bounds'    => ['x' => 60, 'y' => 40, 'width' => 1800, 'height' => 1000],
    'style'     => ['fontName' => 'HelveticaNeue', 'fontSize' => 300, 'bold' => true, 'color' => [255, 255, 255]],
]]

// Countdown that disappears once it has run out
['timer' => [
    'timerUuid'   => '0E45D0AF-BCC2-4A31-BCFD-0F5A3358E225',
    'timerName'   => 'Gottesdienst',
    'format'      => 'mm:ss',
    'visibleWhen' => 'hasTimeRemaining',
]]

Format strings are never written verbatim. In real ProPresenter files TimerText.timer_format_string always carries the literal token ${timer} (and ClockText.clock_format_string the literal ${clock}): that field is the RTF body template, not a time pattern. Writing "mm:ss" there makes ProPresenter print the literal text mm:ss. The real format lives in the structured Timer.Format message (.rv.data.Timer.Format), whose per-component Style enum is only ever NONE (0, hidden) or LONG (2, shown) in real files. The generator therefore emits ${timer} / ${clock} as the format string, derives Timer.Format from the format key, and keeps the element's RTF body a static placeholder.

Slides read back from a .pro file expose hasTimer(), getTimerFormat() (the raw ${timer} token), getTimerFormatMessage() (the structured Timer.Format), getTimerName() and getTimerUuid() (mirroring hasClock() / getClockFormat()).

When visibleWhen is set, the slide additionally exposes hasTimerVisibilityCondition(), getTimerVisibilityCriterion() (returns the same string that was passed in) and getTimerVisibilityTimerUuid().

4. Read a playlist (.proplaylist)

use ProPresenter\Parser\ProPlaylistReader;

$archive = ProPlaylistReader::read('Sunday Service.proplaylist');

echo $archive->getName() . "\n";

foreach ($archive->getEntries() as $entry) {
    echo match ($entry->getType()) {
        'header'       => "── {$entry->getName()} ──\n",
        'presentation' => "  ♪ {$entry->getName()} (arr: " . ($entry->getArrangementName() ?? 'default') . ")\n",
        'placeholder'  => "  · {$entry->getName()} (TBD)\n",
        default        => "  ? {$entry->getName()}\n",
    };

    // Lazily parse embedded .pro files
    if ($entry->getType() === 'presentation') {
        $song = $archive->getEmbeddedSong($entry);
        if ($song !== null) {
            echo "      → " . count($song->getSlides()) . " slides\n";
        }
    }
}

5. Generate a playlist

use ProPresenter\Parser\ProPlaylistGenerator;

ProPlaylistGenerator::generateAndWrite(
    'sunday-service.proplaylist',
    'Sunday Service',
    [
        ['type' => 'header',       'name' => 'Worship',       'color' => [0.95, 0.27, 0.27, 1.0]],
        ['type' => 'presentation', 'name' => 'Amazing Grace', 'path'  => 'file:///Songs/amazing-grace.pro', 'arrangement' => 'normal'],
        ['type' => 'presentation', 'name' => 'Oceans',        'path'  => 'file:///Songs/oceans.pro'],
        ['type' => 'header',       'name' => 'Sermon'],
        ['type' => 'placeholder',  'name' => 'Sermon notes'],
    ],
    ['notes' => 'Sunday morning service'],
);

6. Work with a .probundle

A .probundle is a ZIP archive containing a single .pro file plus its referenced media — perfect for sharing presentations between machines.

use ProPresenter\Parser\ProBundleReader;
use ProPresenter\Parser\ProBundleWriter;
use ProPresenter\Parser\PresentationBundle;
use ProPresenter\Parser\ProFileGenerator;

// Read
$bundle = ProBundleReader::read('Christmas Slides.probundle');
echo $bundle->getName() . "\n";
echo $bundle->getMediaFileCount() . " media files\n";

foreach ($bundle->getMediaFiles() as $filename => $bytes) {
    echo "  $filename: " . strlen($bytes) . " bytes\n";
}

// Build a new bundle (media uses ROOT_CURRENT_RESOURCE → portable across machines)
$song = ProFileGenerator::generate(
    'My Slides',
    [[
        'name'   => 'Background',
        'color'  => [0.2, 0.2, 0.2, 1.0],
        'slides' => [[
            'media'          => 'background.png',
            'format'         => 'png',
            'label'          => 'background.png',
            'bundleRelative' => true,
        ]],
    ]],
    [['name' => 'normal', 'groupNames' => ['Background']]],
);

$bundle = new PresentationBundle(
    $song,
    'My Slides.pro',
    ['background.png' => file_get_contents('background.png')],
);

ProBundleWriter::write($bundle, 'my-slides.probundle');

7. Read a global library file

ProPresenter stores its global library in extension-less protobuf files inside the user library folder. Each is exposed through a dedicated reader/writer:

use ProPresenter\Parser\MacrosFileReader;
use ProPresenter\Parser\MacrosFileWriter;

$library = MacrosFileReader::read('/path/to/Macros');

foreach ($library->getMacros() as $macro) {
    echo $macro->getName() . " — " . $macro->getUuid() . "\n";
}

// Add a macro programmatically
$library->addMacro('Service Start', '00000000-0000-0000-0000-000000000001');
$library->getMacroByName('Service Start')?->setColor(['r' => 0.0, 'g' => 0.5, 'b' => 1.0]);

MacrosFileWriter::write($library, '/path/to/Macros');

The same Reader::read() / Writer::write() pattern applies to every global library file. See doc/api/ for the full set.


CLI Tools

Every supported file type ships with an inspector script in bin/:

php bin/parse-song.php                  path/to/song.pro
php bin/parse-playlist.php              path/to/playlist.proplaylist
php bin/parse-theme.php                 path/to/ThemeFolder
php bin/parse-macros.php                ~/Library/.../Macros
php bin/parse-labels.php                ~/Library/.../Labels
php bin/parse-groups.php                ~/Library/.../Groups
php bin/parse-clear-groups.php          ~/Library/.../ClearGroups
php bin/parse-ccli.php                  ~/Library/.../CCLI
php bin/parse-messages.php              ~/Library/.../Messages
php bin/parse-timers.php                ~/Library/.../Timers
php bin/parse-stage.php                 ~/Library/.../Stage
php bin/parse-workspace.php             ~/Library/.../Workspace
php bin/parse-props.php                 ~/Library/.../Props
php bin/parse-test-patterns.php         ~/Library/.../TestPatterns
php bin/parse-calendar.php              ~/Library/.../Calendar
php bin/parse-key-mappings.php          ~/Library/.../KeyMappings
php bin/parse-communication-devices.php ~/Library/.../CommunicationDevices

Example output for parse-song.php:

Song: Amazing Grace
UUID: A1B2C3D4-...

CCLI Metadata:
  Song Title: Amazing Grace
  Author: John Newton
  Copyright Year: 1779
  Display: yes

Groups (3):
  [1] Verse 1 (2 slides)
      Slide 1: Amazing grace, how sweet the sound / That saved a wretch like me
      Slide 2: I once was lost, but now am found
  [2] Chorus (1 slide)
      Slide 1: My chains are gone, I have been set free
  ...

Arrangements (1):
  [1] normal: Verse 1 -> Chorus -> Verse 1 -> Chorus

Documentation

Full documentation lives in doc/ — start with doc/INDEX.md.

API reference

Topic Document
Songs (.pro) doc/api/song.md
Playlists (.proplaylist) doc/api/playlist.md
Bundles (.probundle) doc/api/bundle.md
Themes (folder) doc/api/theme.md
Macros library doc/api/macros.md
Labels library doc/api/labels.md
Groups library doc/api/groups.md
ClearGroups library doc/api/clear-groups.md
CCLI settings doc/api/ccli.md
Messages library doc/api/messages.md
Timers library doc/api/timers.md
Stage layouts doc/api/stage.md
Workspace doc/api/workspace.md
Props library doc/api/props.md
TestPatterns doc/api/test-patterns.md
Calendar doc/api/calendar.md
KeyMappings doc/api/key-mappings.md
CommunicationDevices doc/api/communication-devices.md

Binary format specifications

Format Document
.pro (songs) doc/formats/pp_song_spec.md
.proplaylist doc/formats/pp_playlist_spec.md
.probundle doc/formats/pp_bundle_spec.md

Search by keyword

Looking for something specific? Use the keyword index: doc/keywords.md.


Project Structure

.
├── bin/                   # 18 CLI tools (parse-*.php scripts)
├── src/                   # PHP source (wrappers, readers, writers, generators)
├── generated/             # Auto-generated protobuf PHP classes (Rv\Data\…)
├── proto/                 # Vendored .proto files (greyshirtguy/ProPresenter7-Proto, Proto 19beta + extras)
├── tests/                 # PHPUnit test suite (369 tests)
├── doc/
│   ├── INDEX.md           # Documentation entry point
│   ├── keywords.md        # Keyword search index
│   ├── CONTRIBUTING.md    # Documentation guidelines
│   ├── api/               # PHP API documentation
│   ├── formats/           # Binary file format specifications
│   ├── internal/          # Development notes (learnings, decisions, issues)
│   └── reference_samples/ # Reference files used by tests (real-world songs)
├── composer.json
├── phpunit.xml
├── LICENSE
└── README.md

Key classes

Class Purpose
ProPresenter\Parser\Song Top-level song wrapper (groups + slides + arrangements)
ProPresenter\Parser\Group Song part (verse, chorus, …)
ProPresenter\Parser\Slide Single slide with text, label, macro, media
ProPresenter\Parser\TextElement Text element with RTF + plain-text accessors
ProPresenter\Parser\Arrangement Group order for a performance
ProPresenter\Parser\PlaylistArchive .proplaylist ZIP wrapper
ProPresenter\Parser\PresentationBundle .probundle ZIP wrapper
ProPresenter\Parser\ThemeBundle Theme folder wrapper
ProPresenter\Parser\ProFileReader / Writer / Generator .pro IO
ProPresenter\Parser\ProPlaylistReader / Writer / Generator .proplaylist IO
ProPresenter\Parser\ProBundleReader / Writer .probundle IO
ProPresenter\Parser\Zip64Fixer Repairs ProPresenter's broken ZIP64 EOCD headers
ProPresenter\Parser\RtfExtractor Standalone CocoaRTF → plain-text converter

Development

Running the tests

composer install
composer test

You should see:

PHPUnit 11.5.55 by Sebastian Bergmann and contributors.

OK (369 tests, 1298 assertions)

The test suite includes:

  • Unit tests — every wrapper class
  • Integration tests — readers + writers round-tripping reference files
  • Mass validation — parses every .pro fixture in doc/reference_samples/all-songs/ (tests/MassValidationTest.php)
  • Binary fidelity tests — verifies byte-perfect round-trips for global library files

Reference samples

Real ProPresenter files used by the tests live in doc/reference_samples/. They are exported from production worship environments and cover edge cases (translations, missing arrangements, ZIP64 quirks, German Unicode, embedded media).

Regenerating sample bundles

Some test fixtures are generated procedurally:

php bin/regen-test-bundles.php

Compatibility & Caveats

  • Verified against ProPresenter 7.16+ on macOS. Files generated by this library open cleanly in ProPresenter 7.
  • Round-trip fidelity — global library files (Macros, Labels, Groups, …) round-trip byte-for-byte. Songs do not: ProPresenter's protobuf schema contains undocumented fields that are dropped on re-encode. The library preserves logical content perfectly, but raw bytes will differ. See doc/internal/issues.md for the gory details.
  • ZIP64 quirk — ProPresenter exports .proplaylist and .probundle files with a 98-byte ZIP64 header offset bug. Zip64Fixer patches this in memory before parsing. Files written by this library use clean standard ZIPs.
  • RTF — slide text is stored as CocoaRTF (Windows-1252 with \'xx hex escapes for non-ASCII). getPlainText() decodes this; the generator produces clean RTF that PP7 accepts.
  • macOS-centric paths — ProPresenter uses file:// URLs with absolute paths in some fields. For portable bundles, use 'bundleRelative' => true on media slides (this sets ROOT_CURRENT_RESOURCE so PP7 resolves media relative to the archive).

Contributing

Contributions are welcome! Please:

  1. Open an issue describing the change before sending a PR for anything non-trivial.
  2. Follow the documentation guidelines in doc/CONTRIBUTING.md.
  3. Add a test for any new behavior — TDD is the convention here.
  4. Run composer test before submitting.
  5. Keep changes focused; avoid unrelated refactors.

License

This project is released under the MIT License.

The bundled .proto files in proto/ are derived from greyshirtguy/ProPresenter7-Proto, Proto 19beta (dumped from ProPresenter v19 beta build 318767123) plus a few extras (calendar, keyMappings, plus three legacy analytics protos retained from the 7.16.2 set), also distributed under the MIT License.


Credits

  • Renewed Vision — for ProPresenter, an excellent presentation tool.
  • greyshirtguy — for reverse-engineering the ProPresenter 7 protobuf schema, without which this library would not exist.
  • Google Protocol Buffers — for the underlying serialization format.

ProPresenter is a trademark of Renewed Vision, LLC. This project is not affiliated with or endorsed by Renewed Vision.