diff --git a/AGENTS.md b/AGENTS.md index 0843902..c3a54eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,8 +18,10 @@ All project documentation lives in `doc/`. Load only what you need. |------|------| | Parse/modify `.pro` song files | `doc/api/song.md` | | Parse/modify `.proplaylist` files | `doc/api/playlist.md` | +| Parse/modify `.probundle` files | `doc/api/bundle.md` | | Understand `.pro` binary format | `doc/formats/pp_song_spec.md` | | Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` | +| Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` | | Debug or troubleshoot | `doc/internal/issues.md` | | Add new documentation | `doc/CONTRIBUTING.md` | @@ -32,10 +34,12 @@ doc/ ├── CONTRIBUTING.md ← How to document new things ├── formats/ ← Binary file format specs │ ├── pp_song_spec.md -│ └── pp_playlist_spec.md +│ ├── pp_playlist_spec.md +│ └── pp_bundle_spec.md ├── api/ ← PHP API docs (read/write/generate) │ ├── song.md -│ └── playlist.md +│ ├── playlist.md +│ └── bundle.md └── internal/ ← Dev notes (learnings, decisions, issues) ├── learnings.md ├── decisions.md @@ -48,6 +52,7 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files: - **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations - **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs +- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets ### CLI Tools diff --git a/doc/INDEX.md b/doc/INDEX.md index 87777ed..08ec4eb 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -8,8 +8,10 @@ |------|------| | Parse/modify `.pro` song files | [api/song.md](api/song.md) | | Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) | +| Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) | | Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) | | Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | +| Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | Add new documentation | [CONTRIBUTING.md](CONTRIBUTING.md) | | Search by keyword | [keywords.md](keywords.md) | @@ -20,10 +22,12 @@ ### File Format Specifications - [formats/pp_song_spec.md](formats/pp_song_spec.md) — ProPresenter 7 `.pro` file format (protobuf structure, RTF handling, field reference) - [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) — ProPresenter 7 `.proplaylist` file format (ZIP64 container, item types) +- [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) — ProPresenter 7 `.probundle` file format (ZIP container, media assets) ### PHP API Documentation - [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files) - [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files) +- [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media) ### Internal Reference - [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered @@ -45,10 +49,12 @@ doc/ ├── CONTRIBUTING.md ← Documentation guidelines ├── formats/ ← File format specifications │ ├── pp_song_spec.md -│ └── pp_playlist_spec.md +│ ├── pp_playlist_spec.md +│ └── pp_bundle_spec.md ├── api/ ← PHP API documentation │ ├── song.md -│ └── playlist.md +│ ├── playlist.md +│ └── bundle.md └── internal/ ← Development notes (optional context) ├── learnings.md ├── decisions.md @@ -69,6 +75,11 @@ Load: doc/api/song.md Load: doc/api/playlist.md ``` +### Task: "Read/write a .probundle" +``` +Load: doc/api/bundle.md +``` + ### Task: "Debug protobuf parsing issues" ``` Load: doc/formats/pp_song_spec.md (sections 2-5) @@ -83,6 +94,7 @@ Load: doc/formats/pp_song_spec.md (section 7: Translations) ### Task: "Fix ZIP64 issues" ``` Load: doc/formats/pp_playlist_spec.md (section 4: ZIP64 Container Format) +Load: doc/formats/pp_bundle_spec.md (section 4: ZIP64 EOCD Quirk) Load: doc/internal/learnings.md (search: Zip64Fixer) ``` @@ -94,6 +106,7 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi - **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations - **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files +- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets ### Key Components @@ -107,6 +120,9 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi | `php/src/ProPlaylistReader.php` | Read `.proplaylist` files | | `php/src/ProPlaylistWriter.php` | Write `.proplaylist` files | | `php/src/ProPlaylistGenerator.php` | Generate `.proplaylist` files from scratch | +| `php/src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) | +| `php/src/ProBundleReader.php` | Read `.probundle` files | +| `php/src/ProBundleWriter.php` | Write `.probundle` files | ### CLI Tools diff --git a/doc/api/bundle.md b/doc/api/bundle.md new file mode 100644 index 0000000..b769dee --- /dev/null +++ b/doc/api/bundle.md @@ -0,0 +1,226 @@ +# Bundle Parser API + +> PHP module for reading, modifying, and writing ProPresenter `.probundle` files. + +## Quick Reference + +```php +use ProPresenter\Parser\ProBundleReader; +use ProPresenter\Parser\ProBundleWriter; +use ProPresenter\Parser\PresentationBundle; + +// Read +$bundle = ProBundleReader::read('path/to/presentation.probundle'); + +// Access +$bundle->getName(); // Presentation name +$bundle->getSong(); // Song wrapper +$bundle->getMediaFiles(); // ['path' => bytes, ...] + +// Write +ProBundleWriter::write($bundle, 'output.probundle'); +``` + +--- + +## Reading Bundles + +```php +use ProPresenter\Parser\ProBundleReader; + +$bundle = ProBundleReader::read('path/to/presentation.probundle'); +``` + +The reader automatically applies `Zip64Fixer` to handle ProPresenter's broken ZIP64 headers. Works with both PP7-exported bundles and library-generated bundles. + +### Metadata Access + +```php +$bundle->getName(); // Presentation name (from embedded Song) +$bundle->getProFilename(); // "SongName.pro" (filename inside archive) +$bundle->getMediaFileCount(); // Number of media files +``` + +--- + +## Presentation Access + +```php +// Get the Song wrapper (same API as ProFileReader) +$song = $bundle->getSong(); +$song->getName(); +$song->getUuid(); +$song->getGroups(); +$song->getSlides(); +$song->getArrangements(); + +// Get the raw protobuf Presentation +$presentation = $bundle->getPresentation(); +``` + +The `Song` object returned by `getSong()` has the same API as songs from `ProFileReader::read()`. See [Song API](song.md) for full details. + +--- + +## Media Files + +```php +// All media files: path => raw bytes +$mediaFiles = $bundle->getMediaFiles(); +foreach ($mediaFiles as $path => $bytes) { + echo "$path: " . strlen($bytes) . " bytes\n"; +} + +// Check if a specific media file exists +if ($bundle->hasMediaFile('/Users/me/Downloads/Media/image.png')) { + $bytes = $bundle->getMediaFile('/Users/me/Downloads/Media/image.png'); +} + +// Count +$bundle->getMediaFileCount(); // 0, 1, 2, ... +``` + +Media file paths are stored as absolute paths with a leading `/` (matching PP7 export format). + +--- + +## Creating Bundles + +Build a `PresentationBundle` from a `Song` and media files: + +```php +use ProPresenter\Parser\PresentationBundle; +use ProPresenter\Parser\ProFileGenerator; +use ProPresenter\Parser\ProBundleWriter; + +// Generate a song with a media slide +$song = ProFileGenerator::generate( + 'My Presentation', + [ + [ + 'name' => 'Background', + 'color' => [0.2, 0.2, 0.2, 1.0], + 'slides' => [ + [ + 'media' => 'file:///Users/me/Downloads/Media/background.png', + 'format' => 'png', + 'label' => 'background.png', + ], + ], + ], + ], + [['name' => 'normal', 'groupNames' => ['Background']]], +); + +// Read the media file +$imageBytes = file_get_contents('/Users/me/Downloads/Media/background.png'); + +// Create the bundle +$bundle = new PresentationBundle( + $song, + 'My Presentation.pro', + ['/Users/me/Downloads/Media/background.png' => $imageBytes], +); + +// Write to disk +ProBundleWriter::write($bundle, 'output.probundle'); +``` + +### Media Path Convention + +Media entries use **absolute paths with a leading `/`**: + +```php +$mediaFiles = [ + '/Users/me/Downloads/Media/background.png' => $pngBytes, + '/Users/me/Downloads/Media/intro.mp4' => $mp4Bytes, +]; +``` + +This matches PP7's export format. The `file:///` URL in the `.pro` protobuf maps to these paths (strip `file://` prefix). + +--- + +## Writing Bundles + +```php +use ProPresenter\Parser\ProBundleWriter; + +ProBundleWriter::write($bundle, 'output.probundle'); +``` + +The writer: +- Creates a standard ZIP archive (deflate compression) +- Writes media entries first, `.pro` file last +- Uses atomic write (temp file + rename) for safety + +--- + +## Round-Trip Example + +```php +use ProPresenter\Parser\ProBundleReader; +use ProPresenter\Parser\ProBundleWriter; + +// Read +$bundle = ProBundleReader::read('input.probundle'); + +// Inspect +echo "Name: " . $bundle->getName() . "\n"; +echo "Media: " . $bundle->getMediaFileCount() . " files\n"; + +// Modify the presentation +$song = $bundle->getSong(); +$song->setName("Modified Presentation"); + +// Write back +ProBundleWriter::write($bundle, 'output.probundle'); +``` + +--- + +## Error Handling + +```php +try { + $bundle = ProBundleReader::read('presentation.probundle'); +} catch (\InvalidArgumentException $e) { + // File not found or empty path + echo "Error: " . $e->getMessage(); +} catch (\RuntimeException $e) { + // Empty file, invalid ZIP, no .pro file found, or invalid protobuf + echo "Error: " . $e->getMessage(); +} +``` + +### Error Cases + +| Condition | Exception | Message Pattern | +|-----------|-----------|-----------------| +| File not found | `InvalidArgumentException` | `Bundle file not found: ...` | +| Empty file | `RuntimeException` | `Bundle file is empty: ...` | +| Invalid ZIP | `RuntimeException` | `Failed to open bundle archive: ...` | +| No `.pro` entry | `RuntimeException` | `No .pro file found in bundle archive: ...` | +| Target dir missing (write) | `InvalidArgumentException` | `Target directory does not exist: ...` | + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `php/src/PresentationBundle.php` | Bundle wrapper (Song + media files) | +| `php/src/ProBundleReader.php` | Reads `.probundle` files (with Zip64Fixer) | +| `php/src/ProBundleWriter.php` | Writes `.probundle` files (standard ZIP) | +| `php/src/ProFileGenerator.php` | Generates `.pro` files with media support | +| `php/src/Zip64Fixer.php` | Fixes ProPresenter ZIP64 header bug | +| `ref/TestBild.probundle` | Generated reference file (PP7-verified) | +| `ref/RestBildExportFromPP.probundle` | PP7-exported reference file | + +--- + +## See Also + +- [Format Specification](../formats/pp_bundle_spec.md) -- Binary format details +- [Song API](song.md) -- `.pro` file handling (same Song object inside bundles) +- [Playlist API](playlist.md) -- `.proplaylist` file handling (similar ZIP pattern) diff --git a/doc/api/playlist.md b/doc/api/playlist.md index d3b2735..efaf66f 100644 --- a/doc/api/playlist.md +++ b/doc/api/playlist.md @@ -227,3 +227,4 @@ See [Format Specification](../formats/pp_playlist_spec.md) Section 4 for details - [Format Specification](../formats/pp_playlist_spec.md) — Binary format details - [Song API](song.md) — `.pro` file handling +- [Bundle API](bundle.md) — `.probundle` file handling (similar ZIP pattern) diff --git a/doc/api/song.md b/doc/api/song.md index bbab407..dc2a29e 100644 --- a/doc/api/song.md +++ b/doc/api/song.md @@ -299,3 +299,4 @@ try { - [Format Specification](../formats/pp_song_spec.md) — Binary format details - [Playlist API](playlist.md) — `.proplaylist` file handling +- [Bundle API](bundle.md) — `.probundle` file handling (Song objects inside bundles) diff --git a/doc/formats/pp_bundle_spec.md b/doc/formats/pp_bundle_spec.md new file mode 100644 index 0000000..e1f5ede --- /dev/null +++ b/doc/formats/pp_bundle_spec.md @@ -0,0 +1,206 @@ +# 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:** + - Media files at **absolute paths with leading `/`** (e.g., `/Users/me/Downloads/Media/image.png`) + - Single `.pro` file at root (filename only, no directory) + +### 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 + +### Entry Layout + +``` +/Users/me/Downloads/pp-test/Media/background.png <-- Media file (absolute path with leading /) +SongName.pro <-- Protobuf-encoded presentation +``` + +### Entry Order +- **Media files first**, then the `.pro` file last +- ProPresenter does not enforce order, but this matches PP7 export behavior + +### Media Entry Naming +- Media entries use **absolute filesystem paths with a leading `/`** +- Standard unzip tools strip the leading `/` with a warning: `warning: stripped absolute path spec from /Users/.../image.png` +- This is intentional and 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 + +Media references in the `.pro` protobuf must use `file:///` absolute URLs with a `LocalRelativePath` in `URL.local`. + +#### URL Structure + +```protobuf +message URL { + string absolute_string = 1; // "file:///Users/me/Downloads/Media/image.png" + LocalRelativePath local = 4; // Root + relative path + Platform platform = 5; // PLATFORM_MACOS +} +``` + +#### LocalRelativePath + +```protobuf +message LocalRelativePath { + Root root = 1; // Enum mapping to macOS directory + string path = 2; // Relative path from root directory +} +``` + +### Root Type Mappings + +| Root Enum | Value | macOS Directory | +|-----------|-------|-----------------| +| `ROOT_BOOT_VOLUME` | 0 | `/` (fallback) | +| `ROOT_USER_HOME` | 2 | `~/` | +| `ROOT_USER_DOWNLOADS` | 4 | `~/Downloads/` | +| `ROOT_USER_DOCUMENTS` | 5 | `~/Documents/` | +| `ROOT_USER_DESKTOP` | 6 | `~/Desktop/` | +| `ROOT_USER_MUSIC` | 7 | `~/Music/` | +| `ROOT_USER_PICTURES` | 8 | `~/Pictures/` | +| `ROOT_USER_VIDEOS` | 9 | `~/Movies/` | +| `ROOT_SHOW` | 10 | ProPresenter library directory | + +### Path Construction Example + +For media at `file:///Users/thorsten/Downloads/pp-test/Media/background.png`: + +``` +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 +``` + +### 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` | +|--------|---------------|-------------| +| **Purpose** | Playlist with multiple songs | Single presentation with media | +| **Compression** | Store only (method 0) | Deflate (default) | +| **Metadata entry** | `data` file (protobuf `rv.data.Playlist`) | None (`.pro` file IS the data) | +| **Song entries** | Multiple `.pro` files | Single `.pro` file | +| **Media paths** | Absolute minus leading `/` | Absolute **with** leading `/` | +| **ZIP64** | Always ZIP64 | Standard ZIP (PP7 exports as 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 at its absolute path. +- **Deduplication:** Same path 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. **Absolute paths with leading `/`:** PP7 stores media at full absolute filesystem paths in the ZIP, including the leading `/` +2. **`URL.local` is required:** PP7 cannot find media without the `LocalRelativePath` in `URL.local` +3. **`file:///` prefix required:** `URL.absolute_string` must use the `file:///` protocol prefix +4. **Lowercase format:** PP7 uses lowercase format strings (`"png"` not `"PNG"`) +5. **Standard ZIP is fine:** PP7 imports standard deflate-compressed ZIPs without issues — the ZIP64/store/permission quirks in PP7 exports are artifacts, not requirements +6. **ZIP64 EOCD bug:** PP7 exports have the same 98-byte offset quirk as `.proplaylist` files + +### What Didn't Work (Rejected Approaches) +- **Relative media paths:** PP7 cannot resolve `Media/image.png` — needs absolute paths +- **Missing `URL.local`:** PP7 shows "image not found" without `LocalRelativePath` +- **Missing `file:///`:** Plain paths like `/Users/me/image.png` are not recognized +- **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** diff --git a/doc/keywords.md b/doc/keywords.md index 0009ba4..73ee37d 100644 --- a/doc/keywords.md +++ b/doc/keywords.md @@ -8,9 +8,11 @@ |---------|----------| | `.pro` | [formats/pp_song_spec.md](formats/pp_song_spec.md) | | `.proplaylist` | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | +| `.probundle` | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | protobuf | [formats/pp_song_spec.md](formats/pp_song_spec.md) | -| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | -| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | +| ZIP | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | +| ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | +| binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | ## Song Structure @@ -26,6 +28,16 @@ | lyrics | [api/song.md](api/song.md) | | CCLI | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | +## Bundle Structure + +| Keyword | Document | +|---------|----------| +| bundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | +| probundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | +| pro6x | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 1 | +| LocalRelativePath | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | +| absolute path | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 2 | + ## Playlist Structure | Keyword | Document | @@ -55,9 +67,9 @@ | Keyword | Document | |---------|----------| | macro | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | -| media | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | -| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | -| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | +| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | +| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | +| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 | | cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | label | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | @@ -65,16 +77,19 @@ | Keyword | Document | |---------|----------| -| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | -| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | +| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | +| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | | generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | -| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | +| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | | ProFileReader | [api/song.md](api/song.md) | | ProFileWriter | [api/song.md](api/song.md) | | ProFileGenerator | [api/song.md](api/song.md) | | ProPlaylistReader | [api/playlist.md](api/playlist.md) | | ProPlaylistWriter | [api/playlist.md](api/playlist.md) | | ProPlaylistGenerator | [api/playlist.md](api/playlist.md) | +| ProBundleReader | [api/bundle.md](api/bundle.md) | +| ProBundleWriter | [api/bundle.md](api/bundle.md) | +| PresentationBundle | [api/bundle.md](api/bundle.md) | | Song | [api/song.md](api/song.md) | | PlaylistArchive | [api/playlist.md](api/playlist.md) | | CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | @@ -98,11 +113,11 @@ | Keyword | Document | |---------|----------| -| error | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | -| exception | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | +| error | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | +| exception | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | | empty file | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8 | | edge case | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 8, [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 9 | -| ZIP64 bug | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 4, [api/playlist.md](api/playlist.md) | +| ZIP64 bug | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 4, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 4, [api/playlist.md](api/playlist.md) | | round-trip | [internal/learnings.md](internal/learnings.md) | | fidelity | [internal/issues.md](internal/issues.md) |