docs(bundle): add .probundle format spec and API documentation
This commit is contained in:
parent
deabfe4ffb
commit
22ebe5fd25
|
|
@ -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
|
||||
|
||||
|
|
|
|||
20
doc/INDEX.md
20
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
|
||||
|
||||
|
|
|
|||
226
doc/api/bundle.md
Normal file
226
doc/api/bundle.md
Normal file
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
206
doc/formats/pp_bundle_spec.md
Normal file
206
doc/formats/pp_bundle_spec.md
Normal file
|
|
@ -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**
|
||||
|
|
@ -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) |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue