diff --git a/packages/core/src/io/formats/fig/export-worker.ts b/packages/core/src/io/formats/fig/export-worker.ts index 64ed6a1d4..d3178a7de 100644 --- a/packages/core/src/io/formats/fig/export-worker.ts +++ b/packages/core/src/io/formats/fig/export-worker.ts @@ -6,10 +6,18 @@ interface CompressMessage { thumbnailPng: Uint8Array metaJson: string images: Array<{ name: string; data: Uint8Array }> + figKiwiVersion?: number } self.onmessage = (e: MessageEvent) => { - const { schemaDeflated, kiwiData, thumbnailPng, metaJson, images } = e.data - const result = compressFigDataSync(schemaDeflated, kiwiData, thumbnailPng, metaJson, images) + const { schemaDeflated, kiwiData, thumbnailPng, metaJson, images, figKiwiVersion } = e.data + const result = compressFigDataSync( + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + images, + figKiwiVersion + ) self.postMessage(result, { transfer: [result.buffer] }) } diff --git a/packages/core/src/io/formats/fig/export.ts b/packages/core/src/io/formats/fig/export.ts index 2bf433ffc..e00a3a526 100644 --- a/packages/core/src/io/formats/fig/export.ts +++ b/packages/core/src/io/formats/fig/export.ts @@ -389,7 +389,8 @@ function compressViaWorker( kiwiData: Uint8Array, thumbnailPng: Uint8Array, metaJson: string, - imageEntries: Array<{ name: string; data: Uint8Array }> + imageEntries: Array<{ name: string; data: Uint8Array }>, + figKiwiVersion?: number ): Promise { return new Promise((resolve, reject) => { const worker = new Worker(new URL('./export-worker.ts', import.meta.url), { @@ -409,7 +410,14 @@ function compressViaWorker( // internal buffer, so transferring kiwiData.buffer or schemaDeflated.buffer detaches // buffers that may be shared with other views, causing "already detached" errors on // subsequent saves. Structured clone (the default) copies the data safely. - worker.postMessage({ schemaDeflated, kiwiData, thumbnailPng, metaJson, images: imageEntries }) + worker.postMessage({ + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + images: imageEntries, + figKiwiVersion + }) }) } @@ -422,7 +430,14 @@ export function compressFigData( figKiwiVersion?: number ): Promise { if (canUseWorker()) { - return compressViaWorker(schemaDeflated, kiwiData, thumbnailPng, metaJson, imageEntries) + return compressViaWorker( + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + imageEntries, + figKiwiVersion + ) } return Promise.resolve( compressFigDataSync( diff --git a/tests/engine/io/fig/export/worker.test.ts b/tests/engine/io/fig/export/worker.test.ts index e9bddb4c0..8ee076d56 100644 --- a/tests/engine/io/fig/export/worker.test.ts +++ b/tests/engine/io/fig/export/worker.test.ts @@ -2,6 +2,8 @@ import { describe, test, expect, beforeAll, setDefaultTimeout } from 'bun:test' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' +import { unzipSync } from 'fflate' + import { parseFigFile, exportFigFile, @@ -15,6 +17,42 @@ import { heavy } from '#tests/helpers/test-utils' setDefaultTimeout(30_000) const FIXTURES = resolve(import.meta.dir, '../../../../fixtures') +const CUSTOM_FIG_KIWI_VERSION = 77 + +function canvasFigVersion(figData: Uint8Array): number { + const zip = unzipSync(figData) + const canvasData = zip['canvas.fig'] ?? zip.canvas + expect(canvasData).toBeDefined() + return new DataView(canvasData.buffer, canvasData.byteOffset, canvasData.byteLength).getUint32( + 8, + true + ) +} + +function compressInWorker(message: { + schemaDeflated: Uint8Array + kiwiData: Uint8Array + thumbnailPng: Uint8Array + metaJson: string + images: Array<{ name: string; data: Uint8Array }> + figKiwiVersion?: number +}): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker( + new URL('../../../../../packages/core/src/io/formats/fig/export-worker.ts', import.meta.url), + { type: 'module' } + ) + worker.onmessage = (event: MessageEvent) => { + worker.terminate() + resolve(event.data) + } + worker.onerror = (event) => { + worker.terminate() + reject(new Error(event.message)) + } + worker.postMessage(message, []) + }) +} describe('fig export compression', () => { test('compressFigDataSync produces valid zip', async () => { @@ -49,6 +87,34 @@ describe('fig export compression', () => { expect(result[0]).toBe(0x50) expect(result[1]).toBe(0x4b) }) + + test('compression preserves custom fig-kiwi version in sync and worker paths', async () => { + const schemaDeflated = new Uint8Array([0x78, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]) + const kiwiData = new Uint8Array([10, 20, 30]) + const thumbnailPng = new Uint8Array([0x89, 0x50]) + const metaJson = JSON.stringify({ version: 1, app: 'test' }) + const images: Array<{ name: string; data: Uint8Array }> = [] + + const syncResult = compressFigDataSync( + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + images, + CUSTOM_FIG_KIWI_VERSION + ) + const workerResult = await compressInWorker({ + schemaDeflated, + kiwiData, + thumbnailPng, + metaJson, + images, + figKiwiVersion: CUSTOM_FIG_KIWI_VERSION + }) + + expect(canvasFigVersion(syncResult)).toBe(CUSTOM_FIG_KIWI_VERSION) + expect(canvasFigVersion(workerResult)).toBe(CUSTOM_FIG_KIWI_VERSION) + }) }) heavy('fig export roundtrip', () => {