From c34d3025a26516fc6d2fe10437fd9e6a8feeb0e7 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Thu, 30 Jul 2026 15:31:17 +0200 Subject: [PATCH] fix(media): encode blurhash with canvas-coerced dimensions --- src/app/workers/media.worker.test.ts | 65 ++++++++++++++++++++++++++++ src/app/workers/media.worker.ts | 4 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/app/workers/media.worker.test.ts diff --git a/src/app/workers/media.worker.test.ts b/src/app/workers/media.worker.test.ts new file mode 100644 index 0000000000..fb2016dba7 --- /dev/null +++ b/src/app/workers/media.worker.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +type Post = { id: number; hash?: string; error?: string }; + +// OffscreenCanvas coerces dimensions to unsigned integers, like the real platform. +class StubOffscreenCanvas { + width: number; + + height: number; + + constructor(width: number, height: number) { + this.width = width >>> 0; + this.height = height >>> 0; + } + + getContext() { + const { width, height } = this; + return { + drawImage: () => undefined, + getImageData: () => ({ + data: new Uint8ClampedArray(width * height * 4), + width, + height, + }), + }; + } +} + +const setupWorker = async () => { + vi.resetModules(); + const posts: Post[] = []; + let messageHandler: ((event: { data: unknown }) => void) | undefined; + vi.stubGlobal('OffscreenCanvas', StubOffscreenCanvas); + vi.stubGlobal('self', { + addEventListener: (type: string, handler: (event: { data: unknown }) => void) => { + if (type === 'message') messageHandler = handler; + }, + postMessage: (message: Post) => posts.push(message), + }); + await import('./media.worker'); + if (!messageHandler) throw new Error('worker did not register a message handler'); + return { posts, messageHandler }; +}; + +describe('media.worker blurhash', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('encodes blurhash when requested dimensions are floats', async () => { + const { posts, messageHandler } = await setupWorker(); + const bitmap = { close: vi.fn<() => void>() }; + + expect(() => + messageHandler({ + data: { id: 1, type: 'blurhash', bitmap, width: 512, height: 170.496 }, + }) + ).not.toThrow(); + + expect(bitmap.close).toHaveBeenCalledOnce(); + expect(posts).toHaveLength(1); + expect(posts[0]?.error).toBeUndefined(); + expect(typeof posts[0]?.hash).toBe('string'); + }); +}); diff --git a/src/app/workers/media.worker.ts b/src/app/workers/media.worker.ts index ffcf84cfec..902d1806e8 100644 --- a/src/app/workers/media.worker.ts +++ b/src/app/workers/media.worker.ts @@ -34,7 +34,9 @@ const handleBlurHash = (req: BlurHashRequest): void => { } ctx.drawImage(bitmap, 0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height); - post({ id: req.id, hash: encode(imageData.data, width, height, 4, 4) }); + // The canvas coerces dimensions to integers, so use imageData's real size: + // encoding with the requested (possibly float) size mismatches the pixels array. + post({ id: req.id, hash: encode(imageData.data, imageData.width, imageData.height, 4, 4) }); } finally { bitmap.close(); }