# 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**
