propresenter-php/doc/formats/pp_bundle_spec.md
Thorsten Bus 8dbcc1bafc feat(bundle): use ROOT_CURRENT_RESOURCE for portable flat media bundles
BREAKING: Bundle media entries are now flat filenames (no directories).
ProBundleWriter flattens all media paths to basename() automatically.
ProFileGenerator supports bundleRelative flag for ROOT_CURRENT_RESOURCE
URLs, enabling bundles that work on any machine without absolute paths.
2026-03-30 10:21:54 +02:00

233 lines
9.4 KiB
Markdown

# ProPresenter 7 `.probundle` 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:** `.probundle`
- **Container Format:** Standard ZIP archive (PKZIP 2.0+, default deflate compression)
- **Binary Format:** Protocol Buffers (Google protobuf v3) for the embedded `.pro` file
- **Top-level Message:** `rv.data.Presentation` (defined in `presentation.proto`)
- **Proto Definitions:** greyshirtguy/ProPresenter7-Proto v7.16.2 (MIT)
- **Predecessor:** Pro6 `.pro6x` format
### Container Structure
- **Archive Type:** Standard ZIP with deflate compression (default)
- **ZIP64 EOCD Quirk:** ProPresenter 7 exports have the same 98-byte EOCD discrepancy as `.proplaylist` files
- **Entry Layout (library output — flat, portable):**
- Media files as **flat filenames** at ZIP root (e.g., `background.png`)
- Single `.pro` file at root (filename only, no directory)
- Protobuf uses `ROOT_CURRENT_RESOURCE` to resolve media relative to the bundle
- **Entry Layout (PP7 export — absolute paths):**
- Media files at **absolute paths with leading `/`** (e.g., `/Users/me/Downloads/Media/image.png`)
- Single `.pro` file at root
### Purpose
A `.probundle` packages a single ProPresenter presentation (`.pro` file) together with all its referenced media assets (images, videos, audio) into a single portable archive. This enables sharing presentations between machines without losing media references.
### File Validity
- **Empty files (0 bytes):** Invalid. Throw exception.
- **Archives without `.pro`:** Invalid. Throw exception.
- **Bundles without media:** Valid. A presentation with no media actions produces a ZIP containing only the `.pro` file.
---
## 2. Archive Structure
### Library Output (Flat — Portable)
```
background.png <-- Media file (flat filename, no directories)
SongName.pro <-- Protobuf-encoded presentation
```
Media entries use **flat filenames only** (no directories, no absolute paths). The `.pro` protobuf references media via `ROOT_CURRENT_RESOURCE`, which PP7 resolves relative to the bundle. This makes bundles fully portable across machines.
### PP7 Export (Absolute Paths)
```
/Users/me/Downloads/pp-test/Media/background.png <-- Absolute path with leading /
SongName.pro <-- Protobuf-encoded presentation
```
PP7's own exports use absolute filesystem paths as ZIP entry names. The reader handles both formats.
### Entry Order
- **Media files first**, then the `.pro` file last
- ProPresenter does not enforce order, but this matches PP7 export behavior
### Compression
- **ProPresenter exports:** Standard deflate compression
- **Writer output:** Standard deflate compression (ZipArchive defaults)
- **No special attributes needed:** Standard permissions, no forced store compression
---
## 3. Protobuf Content (`.pro` File)
### Media URL Format
#### Bundle-Relative (Library Output — Portable)
For bundles, media references use `ROOT_CURRENT_RESOURCE` with just the filename. PP7 resolves this relative to the bundle itself.
```protobuf
message URL {
string absolute_string = 1; // "background.png" (just the filename)
LocalRelativePath local = 4; // ROOT_CURRENT_RESOURCE + filename
Platform platform = 5; // PLATFORM_MACOS
}
```
```
URL.absolute_string = "background.png"
URL.local.root = ROOT_CURRENT_RESOURCE (12)
URL.local.path = "background.png"
URL.platform = PLATFORM_MACOS
```
Both `url` and `image.file.localUrl` use the same structure.
#### Absolute Paths (PP7 Export / Standalone `.pro`)
PP7's own exports and standalone `.pro` files use absolute `file:///` URLs with filesystem-based root mappings:
```
URL.absolute_string = "file:///Users/thorsten/Downloads/pp-test/Media/background.png"
URL.local.root = ROOT_USER_DOWNLOADS (4)
URL.local.path = "pp-test/Media/background.png"
URL.platform = PLATFORM_MACOS
```
#### LocalRelativePath
```protobuf
message LocalRelativePath {
Root root = 1; // Enum mapping to macOS directory or bundle context
string path = 2; // Relative path from root
}
```
### Root Type Mappings
| Root Enum | Value | macOS Directory |
|-----------|-------|-----------------|
| `ROOT_UNKNOWN` | 0 | Unknown |
| `ROOT_BOOT_VOLUME` | 1 | `/` (fallback) |
| `ROOT_USER_HOME` | 2 | `~/` |
| `ROOT_USER_DOCUMENTS` | 3 | `~/Documents/` |
| `ROOT_USER_DOWNLOADS` | 4 | `~/Downloads/` |
| `ROOT_USER_MUSIC` | 5 | `~/Music/` |
| `ROOT_USER_PICTURES` | 6 | `~/Pictures/` |
| `ROOT_USER_VIDEOS` | 7 | `~/Movies/` |
| `ROOT_USER_APP_SUPPORT` | 8 | `~/Library/Application Support/` |
| `ROOT_SHARED` | 9 | `/Users/Shared/` |
| `ROOT_SHOW` | 10 | ProPresenter library directory |
| `ROOT_USER_DESKTOP` | 11 | `~/Desktop/` |
| **`ROOT_CURRENT_RESOURCE`** | **12** | **Relative to current bundle/document** |
### Media Metadata
| Field | Expected Value | Notes |
|-------|---------------|-------|
| `Metadata.format` | Lowercase: `"png"`, `"jpg"`, `"mp4"` | PP7 uses lowercase |
| `Action.type` | `ACTION_TYPE_MEDIA` | Media action type |
| `MediaType.layer_type` | `LAYER_TYPE_FOREGROUND` | Default for slide media |
---
## 4. ZIP64 EOCD Quirk
### Issue
ProPresenter 7 exports `.probundle` files with the same ZIP64 EOCD bug as `.proplaylist` files: a 98-byte discrepancy between the ZIP64 EOCD locator offset and the actual EOCD position.
### Workaround
The reader applies `Zip64Fixer` before opening the archive. This searches backward from the end of file for the ZIP64 EOCD signature (`0x06064b50`) and corrects the offset.
### Writer Behavior
The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates clean archives that PP7 imports without issues.
---
## 5. Differences from `.proplaylist`
| Aspect | `.proplaylist` | `.probundle` (library) | `.probundle` (PP7 export) |
|--------|---------------|----------------------|--------------------------|
| **Purpose** | Playlist with multiple songs | Single presentation with media | Single presentation with media |
| **Compression** | Store only (method 0) | Deflate (default) | Deflate |
| **Metadata entry** | `data` file (protobuf `rv.data.Playlist`) | None (`.pro` file IS the data) | None |
| **Song entries** | Multiple `.pro` files | Single `.pro` file | Single `.pro` file |
| **Media paths** | Absolute minus leading `/` | **Flat filenames** | Absolute with leading `/` |
| **Media URL root** | Filesystem-based roots | `ROOT_CURRENT_RESOURCE (12)` | Filesystem-based roots |
| **ZIP64** | Always ZIP64 | Standard ZIP | ZIP64 |
---
## 6. Edge Cases
### Bundles Without Media
- **Valid.** Archive contains only the `.pro` file.
- **Use case:** Sharing a lyrics-only presentation.
### Multiple Media Files
- **Valid.** Each media file gets its own ZIP entry (flat filename).
- **Deduplication:** Same filename stored once.
### Non-Image Media
- **Videos** (`.mp4`, `.mov`): Same URL format, different `Metadata.format`.
- **Audio** (`.mp3`, `.wav`): Same pattern, `MediaType.audio` field used.
### Case Sensitivity
- `.pro` file detection is case-insensitive (`.pro`, `.Pro`, `.PRO`).
- Media format strings should be **lowercase** to match PP7 behavior.
---
## 7. Reverse-Engineering Evidence
### Reference Files
- **TestBild.probundle:** Generated by this library, verified importable by PP7 with image found
- **RestBildExportFromPP.probundle:** Exported by PP7 after import, used as comparison reference
### Key Discoveries
1. **`ROOT_CURRENT_RESOURCE` (12) enables portable bundles:** PP7 resolves this root relative to the bundle, so media stored as flat filenames in the ZIP are found on any machine
2. **`URL.local` is required:** PP7 cannot find media without the `LocalRelativePath` in `URL.local`
3. **Flat filenames work:** ZIP entries like `image.png` (no directories) with `ROOT_CURRENT_RESOURCE` in the protobuf — PP7 finds the media
4. **Lowercase format:** PP7 uses lowercase format strings (`"png"` not `"PNG"`)
5. **Standard ZIP is fine:** PP7 imports standard deflate-compressed ZIPs without issues
6. **ZIP64 EOCD bug:** PP7 exports have the same 98-byte offset quirk as `.proplaylist` files
7. **PP7 exports use absolute paths:** PP7's own exports use `file:///` absolute paths — but these only work on the same machine. The library uses `ROOT_CURRENT_RESOURCE` for portability instead.
### What Didn't Work (Rejected Approaches)
- **`file:///filename.png` with `ROOT_BOOT_VOLUME`:** PP7 cannot resolve bare filenames with filesystem roots
- **`file:///Users/.../filename.png` with flat ZIP entry:** PP7 needs the URL root to match the ZIP structure
- **`ROOT_SHOW` with bare filename:** PP7 looks in its library dir, not the bundle
- **Missing `URL.local`:** PP7 shows "image not found" without `LocalRelativePath`
- **Uppercase format:** `"PNG"` works but doesn't match PP7's own output
- **Forced store compression / 000 permissions:** Unnecessary hacks that don't affect import
---
## Appendix: PP7 Export vs Library Output
### PP7 Export Characteristics (informational only)
- ZIP64 format with 98-byte EOCD offset bug
- Store compression (method 0)
- File permissions set to `0000`
- These are PP7 artifacts — the library reader handles them, the writer doesn't reproduce them
### Library Output Characteristics
- Standard ZIP (PKZIP 2.0+)
- Deflate compression (ZipArchive default)
- Normal file permissions
- PP7 imports these without issues
---
**End of Specification**