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.
This commit is contained in:
Thorsten Bus 2026-03-30 10:21:54 +02:00
parent 95a2b6984e
commit 8dbcc1bafc
9 changed files with 171 additions and 111 deletions

View file

@ -15,7 +15,7 @@ $bundle = ProBundleReader::read('path/to/presentation.probundle');
// Access // Access
$bundle->getName(); // Presentation name $bundle->getName(); // Presentation name
$bundle->getSong(); // Song wrapper $bundle->getSong(); // Song wrapper
$bundle->getMediaFiles(); // ['path' => bytes, ...] $bundle->getMediaFiles(); // ['filename' => bytes, ...]
// Write // Write
ProBundleWriter::write($bundle, 'output.probundle'); ProBundleWriter::write($bundle, 'output.probundle');
@ -65,22 +65,22 @@ The `Song` object returned by `getSong()` has the same API as songs from `ProFil
## Media Files ## Media Files
```php ```php
// All media files: path => raw bytes // All media files: filename => raw bytes
$mediaFiles = $bundle->getMediaFiles(); $mediaFiles = $bundle->getMediaFiles();
foreach ($mediaFiles as $path => $bytes) { foreach ($mediaFiles as $filename => $bytes) {
echo "$path: " . strlen($bytes) . " bytes\n"; echo "$filename: " . strlen($bytes) . " bytes\n";
} }
// Check if a specific media file exists // Check if a specific media file exists
if ($bundle->hasMediaFile('/Users/me/Downloads/Media/image.png')) { if ($bundle->hasMediaFile('background.png')) {
$bytes = $bundle->getMediaFile('/Users/me/Downloads/Media/image.png'); $bytes = $bundle->getMediaFile('background.png');
} }
// Count // Count
$bundle->getMediaFileCount(); // 0, 1, 2, ... $bundle->getMediaFileCount(); // 0, 1, 2, ...
``` ```
Media file paths are stored as absolute paths with a leading `/` (matching PP7 export format). Media files are stored as flat filenames (no directories). The writer automatically flattens any paths to `basename()`.
--- ---
@ -93,7 +93,7 @@ use ProPresenter\Parser\PresentationBundle;
use ProPresenter\Parser\ProFileGenerator; use ProPresenter\Parser\ProFileGenerator;
use ProPresenter\Parser\ProBundleWriter; use ProPresenter\Parser\ProBundleWriter;
// Generate a song with a media slide // Generate a song with a media slide (bundleRelative for portable bundles)
$song = ProFileGenerator::generate( $song = ProFileGenerator::generate(
'My Presentation', 'My Presentation',
[ [
@ -102,9 +102,10 @@ $song = ProFileGenerator::generate(
'color' => [0.2, 0.2, 0.2, 1.0], 'color' => [0.2, 0.2, 0.2, 1.0],
'slides' => [ 'slides' => [
[ [
'media' => 'file:///Users/me/Downloads/Media/background.png', 'media' => 'background.png',
'format' => 'png', 'format' => 'png',
'label' => 'background.png', 'label' => 'background.png',
'bundleRelative' => true,
], ],
], ],
], ],
@ -113,13 +114,13 @@ $song = ProFileGenerator::generate(
); );
// Read the media file // Read the media file
$imageBytes = file_get_contents('/Users/me/Downloads/Media/background.png'); $imageBytes = file_get_contents('/path/to/background.png');
// Create the bundle // Create the bundle (flat filenames)
$bundle = new PresentationBundle( $bundle = new PresentationBundle(
$song, $song,
'My Presentation.pro', 'My Presentation.pro',
['/Users/me/Downloads/Media/background.png' => $imageBytes], ['background.png' => $imageBytes],
); );
// Write to disk // Write to disk
@ -128,16 +129,28 @@ ProBundleWriter::write($bundle, 'output.probundle');
### Media Path Convention ### Media Path Convention
Media entries use **absolute paths with a leading `/`**: Media entries use **flat filenames** (no directories):
```php ```php
$mediaFiles = [ $mediaFiles = [
'/Users/me/Downloads/Media/background.png' => $pngBytes, 'background.png' => $pngBytes,
'/Users/me/Downloads/Media/intro.mp4' => $mp4Bytes, 'intro.mp4' => $mp4Bytes,
]; ];
``` ```
This matches PP7's export format. The `file:///` URL in the `.pro` protobuf maps to these paths (strip `file://` prefix). The writer flattens any paths to `basename()` automatically. The `.pro` protobuf uses `ROOT_CURRENT_RESOURCE` so PP7 resolves media relative to the bundle — no absolute paths needed.
### `bundleRelative` Slide Option
Set `'bundleRelative' => true` on media slides to use `ROOT_CURRENT_RESOURCE` instead of absolute filesystem paths:
```php
// For bundles (portable — works on any machine)
['media' => 'image.png', 'format' => 'png', 'bundleRelative' => true]
// For standalone .pro files (uses absolute path with filesystem root detection)
['media' => 'file:///Users/me/Downloads/image.png', 'format' => 'png']
```
--- ---
@ -151,6 +164,7 @@ ProBundleWriter::write($bundle, 'output.probundle');
The writer: The writer:
- Creates a standard ZIP archive (deflate compression) - Creates a standard ZIP archive (deflate compression)
- **Flattens media entries to `basename()`** — no directories in the ZIP
- Writes media entries first, `.pro` file last - Writes media entries first, `.pro` file last
- Uses atomic write (temp file + rename) for safety - Uses atomic write (temp file + rename) for safety

View file

@ -19,9 +19,13 @@
### Container Structure ### Container Structure
- **Archive Type:** Standard ZIP with deflate compression (default) - **Archive Type:** Standard ZIP with deflate compression (default)
- **ZIP64 EOCD Quirk:** ProPresenter 7 exports have the same 98-byte EOCD discrepancy as `.proplaylist` files - **ZIP64 EOCD Quirk:** ProPresenter 7 exports have the same 98-byte EOCD discrepancy as `.proplaylist` files
- **Entry Layout:** - **Entry Layout (library output — flat, portable):**
- Media files at **absolute paths with leading `/`** (e.g., `/Users/me/Downloads/Media/image.png`) - Media files as **flat filenames** at ZIP root (e.g., `background.png`)
- Single `.pro` file at root (filename only, no directory) - 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 ### 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. 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.
@ -35,22 +39,28 @@ A `.probundle` packages a single ProPresenter presentation (`.pro` file) togethe
## 2. Archive Structure ## 2. Archive Structure
### Entry Layout ### Library Output (Flat — Portable)
``` ```
/Users/me/Downloads/pp-test/Media/background.png <-- Media file (absolute path with leading /) background.png <-- Media file (flat filename, no directories)
SongName.pro <-- Protobuf-encoded presentation 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 ### Entry Order
- **Media files first**, then the `.pro` file last - **Media files first**, then the `.pro` file last
- ProPresenter does not enforce order, but this matches PP7 export behavior - 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 ### Compression
- **ProPresenter exports:** Standard deflate compression - **ProPresenter exports:** Standard deflate compression
- **Writer output:** Standard deflate compression (ZipArchive defaults) - **Writer output:** Standard deflate compression (ZipArchive defaults)
@ -62,44 +72,30 @@ SongName.pro <-- Protobuf-encoded presen
### Media URL Format ### Media URL Format
Media references in the `.pro` protobuf must use `file:///` absolute URLs with a `LocalRelativePath` in `URL.local`. #### Bundle-Relative (Library Output — Portable)
#### URL Structure For bundles, media references use `ROOT_CURRENT_RESOURCE` with just the filename. PP7 resolves this relative to the bundle itself.
```protobuf ```protobuf
message URL { message URL {
string absolute_string = 1; // "file:///Users/me/Downloads/Media/image.png" string absolute_string = 1; // "background.png" (just the filename)
LocalRelativePath local = 4; // Root + relative path LocalRelativePath local = 4; // ROOT_CURRENT_RESOURCE + filename
Platform platform = 5; // PLATFORM_MACOS Platform platform = 5; // PLATFORM_MACOS
} }
``` ```
#### LocalRelativePath ```
URL.absolute_string = "background.png"
```protobuf URL.local.root = ROOT_CURRENT_RESOURCE (12)
message LocalRelativePath { URL.local.path = "background.png"
Root root = 1; // Enum mapping to macOS directory URL.platform = PLATFORM_MACOS
string path = 2; // Relative path from root directory
}
``` ```
### Root Type Mappings Both `url` and `image.file.localUrl` use the same structure.
| Root Enum | Value | macOS Directory | #### Absolute Paths (PP7 Export / Standalone `.pro`)
|-----------|-------|-----------------|
| `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 PP7's own exports and standalone `.pro` files use absolute `file:///` URLs with filesystem-based root mappings:
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.absolute_string = "file:///Users/thorsten/Downloads/pp-test/Media/background.png"
@ -108,6 +104,33 @@ URL.local.path = "pp-test/Media/background.png"
URL.platform = PLATFORM_MACOS 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 ### Media Metadata
| Field | Expected Value | Notes | | Field | Expected Value | Notes |
@ -133,14 +156,15 @@ The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates cl
## 5. Differences from `.proplaylist` ## 5. Differences from `.proplaylist`
| Aspect | `.proplaylist` | `.probundle` | | Aspect | `.proplaylist` | `.probundle` (library) | `.probundle` (PP7 export) |
|--------|---------------|-------------| |--------|---------------|----------------------|--------------------------|
| **Purpose** | Playlist with multiple songs | Single presentation with media | | **Purpose** | Playlist with multiple songs | Single presentation with media | Single presentation with media |
| **Compression** | Store only (method 0) | Deflate (default) | | **Compression** | Store only (method 0) | Deflate (default) | Deflate |
| **Metadata entry** | `data` file (protobuf `rv.data.Playlist`) | None (`.pro` file IS the data) | | **Metadata entry** | `data` file (protobuf `rv.data.Playlist`) | None (`.pro` file IS the data) | None |
| **Song entries** | Multiple `.pro` files | Single `.pro` file | | **Song entries** | Multiple `.pro` files | Single `.pro` file | Single `.pro` file |
| **Media paths** | Absolute minus leading `/` | Absolute **with** leading `/` | | **Media paths** | Absolute minus leading `/` | **Flat filenames** | Absolute with leading `/` |
| **ZIP64** | Always ZIP64 | Standard ZIP (PP7 exports as ZIP64) | | **Media URL root** | Filesystem-based roots | `ROOT_CURRENT_RESOURCE (12)` | Filesystem-based roots |
| **ZIP64** | Always ZIP64 | Standard ZIP | ZIP64 |
--- ---
@ -151,8 +175,8 @@ The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates cl
- **Use case:** Sharing a lyrics-only presentation. - **Use case:** Sharing a lyrics-only presentation.
### Multiple Media Files ### Multiple Media Files
- **Valid.** Each media file gets its own ZIP entry at its absolute path. - **Valid.** Each media file gets its own ZIP entry (flat filename).
- **Deduplication:** Same path stored once. - **Deduplication:** Same filename stored once.
### Non-Image Media ### Non-Image Media
- **Videos** (`.mp4`, `.mov`): Same URL format, different `Metadata.format`. - **Videos** (`.mp4`, `.mov`): Same URL format, different `Metadata.format`.
@ -171,17 +195,19 @@ The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates cl
- **RestBildExportFromPP.probundle:** Exported by PP7 after import, used as comparison reference - **RestBildExportFromPP.probundle:** Exported by PP7 after import, used as comparison reference
### Key Discoveries ### Key Discoveries
1. **Absolute paths with leading `/`:** PP7 stores media at full absolute filesystem paths in the ZIP, including the leading `/` 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` 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 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"`) 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 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 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) ### What Didn't Work (Rejected Approaches)
- **Relative media paths:** PP7 cannot resolve `Media/image.png` — needs absolute paths - **`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` - **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 - **Uppercase format:** `"PNG"` works but doesn't match PP7's own output
- **Forced store compression / 000 permissions:** Unnecessary hacks that don't affect import - **Forced store compression / 000 permissions:** Unnecessary hacks that don't affect import

View file

@ -17,7 +17,6 @@ $imageBytes = file_get_contents($tmpPng);
unlink($tmpPng); unlink($tmpPng);
$refDir = dirname(__DIR__, 2) . '/ref'; $refDir = dirname(__DIR__, 2) . '/ref';
$mediaAbsPath = '/Users/thorsten/AI/propresenter/ref/Media/test-background.png';
$song = ProFileGenerator::generate( $song = ProFileGenerator::generate(
'TestBild', 'TestBild',
@ -27,8 +26,10 @@ $song = ProFileGenerator::generate(
'color' => [0.0, 0.0, 0.0, 1.0], 'color' => [0.0, 0.0, 0.0, 1.0],
'slides' => [ 'slides' => [
[ [
'media' => 'file://' . $mediaAbsPath, 'media' => 'test-background.png',
'format' => 'png', 'format' => 'png',
'label' => 'test-background.png',
'bundleRelative' => true,
], ],
], ],
], ],
@ -36,6 +37,6 @@ $song = ProFileGenerator::generate(
[['name' => 'normal', 'groupNames' => ['Verse 1']]], [['name' => 'normal', 'groupNames' => ['Verse 1']]],
); );
$bundle = new PresentationBundle($song, 'TestBild.pro', [$mediaAbsPath => $imageBytes]); $bundle = new PresentationBundle($song, 'TestBild.pro', ['test-background.png' => $imageBytes]);
ProBundleWriter::write($bundle, $refDir . '/TestBild.probundle'); ProBundleWriter::write($bundle, $refDir . '/TestBild.probundle');
echo "TestBild.probundle written\n"; echo "TestBild.probundle written\n";

View file

@ -13,16 +13,16 @@ use Rv\Data\Presentation;
* together with all its referenced media assets (images, videos, audio). * together with all its referenced media assets (images, videos, audio).
* This is the Pro7 successor to the Pro6 .pro6x format. * This is the Pro7 successor to the Pro6 .pro6x format.
* *
* Archive layout: * Archive layout (flat no directories):
* image.jpg Media files (basename only)
* video.mp4
* SongName.pro Protobuf-encoded presentation * SongName.pro Protobuf-encoded presentation
* Media/image.jpg Referenced media files
* Media/video.mp4
*/ */
class PresentationBundle class PresentationBundle
{ {
private Song $song; private Song $song;
/** @var array<string, string> relative path => raw bytes */ /** @var array<string, string> filename => raw bytes */
private array $mediaFiles; private array $mediaFiles;
private string $proFilename; private string $proFilename;
@ -74,7 +74,7 @@ class PresentationBundle
/** /**
* All media files in the bundle. * All media files in the bundle.
* *
* @return array<string, string> relative path => raw bytes * @return array<string, string> filename => raw bytes
*/ */
public function getMediaFiles(): array public function getMediaFiles(): array
{ {

View file

@ -33,7 +33,7 @@ final class ProBundleWriter
$isOpen = true; $isOpen = true;
foreach ($bundle->getMediaFiles() as $entryName => $contents) { foreach ($bundle->getMediaFiles() as $entryName => $contents) {
self::addEntry($zip, $entryName, $contents, $filePath); self::addEntry($zip, basename($entryName), $contents, $filePath);
} }
$proBytes = $bundle->getPresentation()->serializeToString(); $proBytes = $bundle->getPresentation()->serializeToString();

View file

@ -228,6 +228,7 @@ final class ProFileGenerator
$mediaName, $mediaName,
(int) ($slideData['mediaWidth'] ?? 0), (int) ($slideData['mediaWidth'] ?? 0),
(int) ($slideData['mediaHeight'] ?? 0), (int) ($slideData['mediaHeight'] ?? 0),
(bool) ($slideData['bundleRelative'] ?? false),
); );
} }
@ -281,17 +282,27 @@ final class ProFileGenerator
return $action; return $action;
} }
private static function buildMediaAction(string $absoluteUrl, string $format, ?string $name = null, int $imageWidth = 0, int $imageHeight = 0): Action private static function buildMediaAction(string $absoluteUrl, string $format, ?string $name = null, int $imageWidth = 0, int $imageHeight = 0, bool $bundleRelative = false): Action
{ {
if ($bundleRelative) {
$filename = basename($absoluteUrl);
$url = self::buildBundleRelativeUrl($filename);
$fileLocalUrl = self::buildBundleRelativeUrl($filename);
} else {
$url = new URL(); $url = new URL();
$url->setAbsoluteString($absoluteUrl); $url->setAbsoluteString($absoluteUrl);
$url->setLocal(self::buildLocalRelativePath($absoluteUrl)); $url->setLocal(self::buildLocalRelativePath($absoluteUrl));
$url->setPlatform(UrlPlatform::PLATFORM_MACOS); $url->setPlatform(UrlPlatform::PLATFORM_MACOS);
$fileLocalUrl = new URL();
$fileLocalUrl->setAbsoluteString($absoluteUrl);
$fileLocalUrl->setLocal(self::buildLocalRelativePath($absoluteUrl));
$fileLocalUrl->setPlatform(UrlPlatform::PLATFORM_MACOS);
}
$metadata = new Metadata(); $metadata = new Metadata();
$metadata->setFormat($format); $metadata->setFormat($format);
// Build the image type properties with drawing + file
$naturalSize = new Size(); $naturalSize = new Size();
$naturalSize->setWidth($imageWidth); $naturalSize->setWidth($imageWidth);
$naturalSize->setHeight($imageHeight); $naturalSize->setHeight($imageHeight);
@ -310,11 +321,6 @@ final class ProFileGenerator
$drawing->setCropInsets($cropInsets); $drawing->setCropInsets($cropInsets);
$drawing->setAlphaType(AlphaType::ALPHA_TYPE_STRAIGHT); $drawing->setAlphaType(AlphaType::ALPHA_TYPE_STRAIGHT);
$fileLocalUrl = new URL();
$fileLocalUrl->setAbsoluteString($absoluteUrl);
$fileLocalUrl->setLocal(self::buildLocalRelativePath($absoluteUrl));
$fileLocalUrl->setPlatform(UrlPlatform::PLATFORM_MACOS);
$fileProperties = new FileProperties(); $fileProperties = new FileProperties();
$fileProperties->setLocalUrl($fileLocalUrl); $fileProperties->setLocalUrl($fileLocalUrl);
@ -346,6 +352,20 @@ final class ProFileGenerator
return $action; return $action;
} }
private static function buildBundleRelativeUrl(string $filename): URL
{
$local = new LocalRelativePath();
$local->setRoot(LocalRelativePath\Root::ROOT_CURRENT_RESOURCE);
$local->setPath($filename);
$url = new URL();
$url->setAbsoluteString($filename);
$url->setLocal($local);
$url->setPlatform(UrlPlatform::PLATFORM_MACOS);
return $url;
}
private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null): SlideElement private static function buildSlideElement(string $name, string $text, ?Rect $bounds = null): SlideElement
{ {
$graphicsElement = new GraphicsElement(); $graphicsElement = new GraphicsElement();

View file

@ -105,7 +105,7 @@ class ProBundleTest extends TestCase
$bundle = new PresentationBundle( $bundle = new PresentationBundle(
$song, $song,
'Bundle Test Song.pro', 'Bundle Test Song.pro',
['Media/test-background.png' => $imageBytes], ['test-background.png' => $imageBytes],
); );
$bundlePath = $this->tmpDir . '/BundleTestSong.probundle'; $bundlePath = $this->tmpDir . '/BundleTestSong.probundle';
@ -117,7 +117,7 @@ class ProBundleTest extends TestCase
$zip = new ZipArchive(); $zip = new ZipArchive();
$this->assertTrue($zip->open($bundlePath) === true); $this->assertTrue($zip->open($bundlePath) === true);
$this->assertNotFalse($zip->locateName('Bundle Test Song.pro')); $this->assertNotFalse($zip->locateName('Bundle Test Song.pro'));
$this->assertNotFalse($zip->locateName('Media/test-background.png')); $this->assertNotFalse($zip->locateName('test-background.png'));
$this->assertSame(2, $zip->numFiles); $this->assertSame(2, $zip->numFiles);
$zip->close(); $zip->close();
@ -126,8 +126,8 @@ class ProBundleTest extends TestCase
$this->assertSame('Bundle Test Song', $readBundle->getName()); $this->assertSame('Bundle Test Song', $readBundle->getName());
$this->assertSame('Bundle Test Song.pro', $readBundle->getProFilename()); $this->assertSame('Bundle Test Song.pro', $readBundle->getProFilename());
$this->assertSame(1, $readBundle->getMediaFileCount()); $this->assertSame(1, $readBundle->getMediaFileCount());
$this->assertTrue($readBundle->hasMediaFile('Media/test-background.png')); $this->assertTrue($readBundle->hasMediaFile('test-background.png'));
$this->assertSame($imageBytes, $readBundle->getMediaFile('Media/test-background.png')); $this->assertSame($imageBytes, $readBundle->getMediaFile('test-background.png'));
$readSong = $readBundle->getSong(); $readSong = $readBundle->getSong();
$this->assertSame('Bundle Test Song', $readSong->getName()); $this->assertSame('Bundle Test Song', $readSong->getName());
@ -168,8 +168,8 @@ class ProBundleTest extends TestCase
$song, $song,
'Multi Media Song.pro', 'Multi Media Song.pro',
[ [
'Media/slide1.png' => $image1Bytes, 'slide1.png' => $image1Bytes,
'Media/slide2.png' => $image2Bytes, 'slide2.png' => $image2Bytes,
], ],
); );
@ -179,10 +179,10 @@ class ProBundleTest extends TestCase
$readBundle = ProBundleReader::read($bundlePath); $readBundle = ProBundleReader::read($bundlePath);
$this->assertSame(2, $readBundle->getMediaFileCount()); $this->assertSame(2, $readBundle->getMediaFileCount());
$this->assertTrue($readBundle->hasMediaFile('Media/slide1.png')); $this->assertTrue($readBundle->hasMediaFile('slide1.png'));
$this->assertTrue($readBundle->hasMediaFile('Media/slide2.png')); $this->assertTrue($readBundle->hasMediaFile('slide2.png'));
$this->assertSame($image1Bytes, $readBundle->getMediaFile('Media/slide1.png')); $this->assertSame($image1Bytes, $readBundle->getMediaFile('slide1.png'));
$this->assertSame($image2Bytes, $readBundle->getMediaFile('Media/slide2.png')); $this->assertSame($image2Bytes, $readBundle->getMediaFile('slide2.png'));
} }
#[Test] #[Test]
@ -238,15 +238,13 @@ class ProBundleTest extends TestCase
} }
#[Test] #[Test]
public function writeProducesStandardZipWithAbsoluteMediaPaths(): void public function writeProducesStandardZipWithFlatMediaPaths(): void
{ {
$imagePath = $this->tmpDir . '/bg.png'; $imagePath = $this->tmpDir . '/bg.png';
$this->createTestPngImage($imagePath, 100, 100); $this->createTestPngImage($imagePath, 100, 100);
$imageBytes = file_get_contents($imagePath); $imageBytes = file_get_contents($imagePath);
$this->assertNotFalse($imageBytes); $this->assertNotFalse($imageBytes);
$mediaAbsPath = '/Users/test/Media/bg.png';
$song = ProFileGenerator::generate( $song = ProFileGenerator::generate(
'ZipFormatTest', 'ZipFormatTest',
[ [
@ -255,8 +253,9 @@ class ProBundleTest extends TestCase
'color' => [0, 0, 0, 1], 'color' => [0, 0, 0, 1],
'slides' => [ 'slides' => [
[ [
'media' => 'file://' . $mediaAbsPath, 'media' => 'bg.png',
'format' => 'png', 'format' => 'png',
'bundleRelative' => true,
], ],
], ],
], ],
@ -267,7 +266,7 @@ class ProBundleTest extends TestCase
$bundle = new PresentationBundle( $bundle = new PresentationBundle(
$song, $song,
'ZipFormatTest.pro', 'ZipFormatTest.pro',
[$mediaAbsPath => $imageBytes], ['bg.png' => $imageBytes],
); );
$bundlePath = $this->tmpDir . '/ZipFormatTest.probundle'; $bundlePath = $this->tmpDir . '/ZipFormatTest.probundle';
@ -277,8 +276,8 @@ class ProBundleTest extends TestCase
$this->assertTrue($zip->open($bundlePath) === true); $this->assertTrue($zip->open($bundlePath) === true);
$this->assertSame(2, $zip->numFiles); $this->assertSame(2, $zip->numFiles);
$mediaIdx = $zip->locateName($mediaAbsPath); $mediaIdx = $zip->locateName('bg.png');
$this->assertNotFalse($mediaIdx, 'Media entry should use absolute path with leading /'); $this->assertNotFalse($mediaIdx, 'Media entry should use flat filename');
$proIdx = $zip->locateName('ZipFormatTest.pro'); $proIdx = $zip->locateName('ZipFormatTest.pro');
$this->assertNotFalse($proIdx); $this->assertNotFalse($proIdx);
@ -288,8 +287,8 @@ class ProBundleTest extends TestCase
$readBundle = ProBundleReader::read($bundlePath); $readBundle = ProBundleReader::read($bundlePath);
$this->assertSame('ZipFormatTest', $readBundle->getName()); $this->assertSame('ZipFormatTest', $readBundle->getName());
$this->assertTrue($readBundle->hasMediaFile($mediaAbsPath)); $this->assertTrue($readBundle->hasMediaFile('bg.png'));
$this->assertSame($imageBytes, $readBundle->getMediaFile($mediaAbsPath)); $this->assertSame($imageBytes, $readBundle->getMediaFile('bg.png'));
} }
#[Test] #[Test]
@ -310,7 +309,7 @@ class ProBundleTest extends TestCase
$bundle = new PresentationBundle( $bundle = new PresentationBundle(
$song, $song,
'Wrapper Test.pro', 'Wrapper Test.pro',
['Media/bg.jpg' => 'fake-jpeg-bytes'], ['bg.jpg' => 'fake-jpeg-bytes'],
); );
$this->assertSame('Wrapper Test', $bundle->getName()); $this->assertSame('Wrapper Test', $bundle->getName());
@ -318,9 +317,9 @@ class ProBundleTest extends TestCase
$this->assertSame($song, $bundle->getSong()); $this->assertSame($song, $bundle->getSong());
$this->assertSame($song->getPresentation(), $bundle->getPresentation()); $this->assertSame($song->getPresentation(), $bundle->getPresentation());
$this->assertSame(1, $bundle->getMediaFileCount()); $this->assertSame(1, $bundle->getMediaFileCount());
$this->assertTrue($bundle->hasMediaFile('Media/bg.jpg')); $this->assertTrue($bundle->hasMediaFile('bg.jpg'));
$this->assertSame('fake-jpeg-bytes', $bundle->getMediaFile('Media/bg.jpg')); $this->assertSame('fake-jpeg-bytes', $bundle->getMediaFile('bg.jpg'));
$this->assertSame(['Media/bg.jpg' => 'fake-jpeg-bytes'], $bundle->getMediaFiles()); $this->assertSame(['bg.jpg' => 'fake-jpeg-bytes'], $bundle->getMediaFiles());
} }
private function createTestPngImage(string $path, int $width, int $height): void private function createTestPngImage(string $path, int $width, int $height): void

View file

@ -1133,7 +1133,7 @@ class ProFileGeneratorTest extends TestCase
$bundle = new PresentationBundle( $bundle = new PresentationBundle(
$song, $song,
'TestBild.pro', 'TestBild.pro',
['Media/test-background.png' => $fakeImageBytes], ['test-background.png' => $fakeImageBytes],
); );
$path = $this->tmpDir . '/TestBild.probundle'; $path = $this->tmpDir . '/TestBild.probundle';
@ -1145,7 +1145,7 @@ class ProFileGeneratorTest extends TestCase
$this->assertSame('TestBild', $read->getName()); $this->assertSame('TestBild', $read->getName());
$this->assertSame('TestBild.pro', $read->getProFilename()); $this->assertSame('TestBild.pro', $read->getProFilename());
$this->assertSame(1, $read->getMediaFileCount()); $this->assertSame(1, $read->getMediaFileCount());
$this->assertTrue($read->hasMediaFile('Media/test-background.png')); $this->assertTrue($read->hasMediaFile('test-background.png'));
// UUID preserved through serialization // UUID preserved through serialization
$this->assertSame( $this->assertSame(

Binary file not shown.