Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/layout-engine/pm-adapter/src/utilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,40 @@ describe('Media Utilities', () => {
const result = hydrateImageBlocks(blocks, mediaFiles);
expect(result[0].src).toBe('data:image/png;base64,iVBORw0KGgoAAAANS');
});

it('handles Uint8Array media values from persistence layers', () => {
const blocks: FlowBlock[] = [
{
kind: 'image',
id: '1',
src: 'media/image.png',
runs: [],
},
];
// Persistence layers (e.g., Y.js binary encoding) may return Uint8Array
const base64String = 'iVBORw0KGgoAAAANS';
const mediaFiles = { 'media/image.png': new TextEncoder().encode(base64String) };

const result = hydrateImageBlocks(blocks, mediaFiles);
expect(result[0].src).toBe('data:image/png;base64,iVBORw0KGgoAAAANS');
});

it('handles Uint8Array data URI values from persistence layers', () => {
const blocks: FlowBlock[] = [
{
kind: 'image',
id: '1',
src: 'media/image.png',
runs: [],
},
];
// Data URI stored as Uint8Array
const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANS';
const mediaFiles = { 'media/image.png': new TextEncoder().encode(dataUri) };

const result = hydrateImageBlocks(blocks, mediaFiles);
expect(result[0].src).toBe('data:image/png;base64,iVBORw0KGgoAAAANS');
});
});

describe('hydrateImageBlocks - ShapeGroup image hydration', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/layout-engine/pm-adapter/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@ export function inferExtensionFromPath(value?: string | null): string | undefine
* // Matches via candidate path: word/media/rId3.png
* ```
*/
export function hydrateImageBlocks(blocks: FlowBlock[], mediaFiles?: Record<string, string>): FlowBlock[] {
export function hydrateImageBlocks(blocks: FlowBlock[], mediaFiles?: Record<string, string | Uint8Array>): FlowBlock[] {
if (!mediaFiles || Object.keys(mediaFiles).length === 0) {
return blocks;
}
Expand All @@ -963,7 +963,9 @@ export function hydrateImageBlocks(blocks: FlowBlock[], mediaFiles?: Record<stri
Object.entries(mediaFiles).forEach(([key, value]) => {
const normalized = normalizeMediaKey(key);
if (normalized) {
normalizedMedia.set(normalized, value);
// Handle Uint8Array values from persistence layers (e.g., Y.js binary encoding)
const stringValue = value instanceof Uint8Array ? new TextDecoder().decode(value) : value;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Base64-encode binary media bytes before hydrating images

When mediaFiles contains raw image bytes (a common Uint8Array shape from persistence/blob stores), decoding with new TextDecoder().decode(value) produces UTF-8 text, not base64, so resolveImageSrc later emits data:image/...;base64,<non-base64> and the image fails to render. This fix handles UTF-8 encoded base64 strings, but it still breaks true binary Uint8Array payloads; hydrateImageBlocks should base64-encode binary bytes (or detect ASCII/base64 vs binary) before storing in normalizedMedia.

Useful? React with 👍 / 👎.

normalizedMedia.set(normalized, stringValue);
}
});

Expand Down
Loading