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
$bundle->getName(); // Presentation name
$bundle->getSong(); // Song wrapper
$bundle->getMediaFiles(); // ['path' => bytes, ...]
$bundle->getMediaFiles(); // ['filename' => bytes, ...]
// Write
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
```php
// All media files: path => raw bytes
// All media files: filename => raw bytes
$mediaFiles = $bundle->getMediaFiles();
foreach ($mediaFiles as $path => $bytes) {
echo "$path: " . strlen($bytes) . " bytes\n";
foreach ($mediaFiles as $filename => $bytes) {
echo "$filename: " . 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');
if ($bundle->hasMediaFile('background.png')) {
$bytes = $bundle->getMediaFile('background.png');
}
// Count
$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\ProBundleWriter;
// Generate a song with a media slide
// Generate a song with a media slide (bundleRelative for portable bundles)
$song = ProFileGenerator::generate(
'My Presentation',
[
@ -102,9 +102,10 @@ $song = ProFileGenerator::generate(
'color' => [0.2, 0.2, 0.2, 1.0],
'slides' => [
[
'media' => 'file:///Users/me/Downloads/Media/background.png',
'media' => 'background.png',
'format' => 'png',
'label' => 'background.png',
'bundleRelative' => true,
],
],
],
@ -113,13 +114,13 @@ $song = ProFileGenerator::generate(
);
// 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(
$song,
'My Presentation.pro',
['/Users/me/Downloads/Media/background.png' => $imageBytes],
['background.png' => $imageBytes],
);
// Write to disk
@ -128,16 +129,28 @@ ProBundleWriter::write($bundle, 'output.probundle');
### Media Path Convention
Media entries use **absolute paths with a leading `/`**:
Media entries use **flat filenames** (no directories):
```php
$mediaFiles = [
'/Users/me/Downloads/Media/background.png' => $pngBytes,
'/Users/me/Downloads/Media/intro.mp4' => $mp4Bytes,
'background.png' => $pngBytes,
'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:
- Creates a standard ZIP archive (deflate compression)
- **Flattens media entries to `basename()`** — no directories in the ZIP
- Writes media entries first, `.pro` file last
- Uses atomic write (temp file + rename) for safety

View file

@ -19,9 +19,13 @@
### 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`)
- **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.
@ -35,22 +39,28 @@ A `.probundle` packages a single ProPresenter presentation (`.pro` file) togethe
## 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
```
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
### 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)
@ -62,44 +72,30 @@ SongName.pro <-- Protobuf-encoded presen
### 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
message URL {
string absolute_string = 1; // "file:///Users/me/Downloads/Media/image.png"
LocalRelativePath local = 4; // Root + relative path
string absolute_string = 1; // "background.png" (just the filename)
LocalRelativePath local = 4; // ROOT_CURRENT_RESOURCE + filename
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
}
```
URL.absolute_string = "background.png"
URL.local.root = ROOT_CURRENT_RESOURCE (12)
URL.local.path = "background.png"
URL.platform = PLATFORM_MACOS
```
### Root Type Mappings
Both `url` and `image.file.localUrl` use the same structure.
| 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 |
#### Absolute Paths (PP7 Export / Standalone `.pro`)
### Path Construction Example
For media at `file:///Users/thorsten/Downloads/pp-test/Media/background.png`:
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"
@ -108,6 +104,33 @@ 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 |
@ -133,14 +156,15 @@ The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates cl
## 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) |
| 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 |
---
@ -151,8 +175,8 @@ The writer produces standard ZIPs without the bug. PHP's `ZipArchive` creates cl
- **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.
- **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`.
@ -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
### 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`
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"`)
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
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)
- **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 `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

View file

@ -17,7 +17,6 @@ $imageBytes = file_get_contents($tmpPng);
unlink($tmpPng);
$refDir = dirname(__DIR__, 2) . '/ref';
$mediaAbsPath = '/Users/thorsten/AI/propresenter/ref/Media/test-background.png';
$song = ProFileGenerator::generate(
'TestBild',
@ -27,8 +26,10 @@ $song = ProFileGenerator::generate(
'color' => [0.0, 0.0, 0.0, 1.0],
'slides' => [
[
'media' => 'file://' . $mediaAbsPath,
'media' => 'test-background.png',
'format' => 'png',
'label' => 'test-background.png',
'bundleRelative' => true,
],
],
],
@ -36,6 +37,6 @@ $song = ProFileGenerator::generate(
[['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');
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).
* 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
* Media/image.jpg Referenced media files
* Media/video.mp4
*/
class PresentationBundle
{
private Song $song;
/** @var array<string, string> relative path => raw bytes */
/** @var array<string, string> filename => raw bytes */
private array $mediaFiles;
private string $proFilename;
@ -74,7 +74,7 @@ class PresentationBundle
/**
* 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
{

View file

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

View file

@ -228,6 +228,7 @@ final class ProFileGenerator
$mediaName,
(int) ($slideData['mediaWidth'] ?? 0),
(int) ($slideData['mediaHeight'] ?? 0),
(bool) ($slideData['bundleRelative'] ?? false),
);
}
@ -281,17 +282,27 @@ final class ProFileGenerator
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
{
$url = new URL();
$url->setAbsoluteString($absoluteUrl);
$url->setLocal(self::buildLocalRelativePath($absoluteUrl));
$url->setPlatform(UrlPlatform::PLATFORM_MACOS);
if ($bundleRelative) {
$filename = basename($absoluteUrl);
$url = self::buildBundleRelativeUrl($filename);
$fileLocalUrl = self::buildBundleRelativeUrl($filename);
} else {
$url = new URL();
$url->setAbsoluteString($absoluteUrl);
$url->setLocal(self::buildLocalRelativePath($absoluteUrl));
$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->setFormat($format);
// Build the image type properties with drawing + file
$naturalSize = new Size();
$naturalSize->setWidth($imageWidth);
$naturalSize->setHeight($imageHeight);
@ -310,11 +321,6 @@ final class ProFileGenerator
$drawing->setCropInsets($cropInsets);
$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->setLocalUrl($fileLocalUrl);
@ -346,6 +352,20 @@ final class ProFileGenerator
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
{
$graphicsElement = new GraphicsElement();

View file

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

View file

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

Binary file not shown.