From 139ee32d788d8702e32ea5288272ecdc900bb087 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 2 Feb 2026 17:12:11 -0800 Subject: [PATCH] fix: properly parse PNG iTXt chunks per specification (#8530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes PNG iTXt chunk parsing to comply with the PNG specification. ## Problem The current iTXt parser incorrectly reads the text content immediately after the keyword null terminator, but iTXt chunks have additional fields: - Compression flag (1 byte) - Compression method (1 byte) - Language tag (null-terminated) - Translated keyword (null-terminated) - Text content This caused PNGs that correctly follow the spec to fail loading with JSON parse errors due to leading null bytes. ## Solution Skip the compression flag, method, language tag, and translated keyword fields before reading the text content. Fixes #8150 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8530-fix-properly-parse-PNG-iTXt-chunks-per-specification-2fa6d73d36508189bef4cc5fa3899096) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action Co-authored-by: github-actions --- .../assets/workflowInMedia/workflow_itxt.png | Bin 0 -> 517 bytes .../tests/loadWorkflowInMedia.spec.ts | 1 + .../workflow-itxt-png-chromium-linux.png | Bin 0 -> 42880 bytes src/scripts/metadata/png.test.ts | 245 ++++++++++++++++++ src/scripts/metadata/png.ts | 119 +++++++-- 5 files changed, 344 insertions(+), 21 deletions(-) create mode 100644 browser_tests/assets/workflowInMedia/workflow_itxt.png create mode 100644 browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-itxt-png-chromium-linux.png create mode 100644 src/scripts/metadata/png.test.ts diff --git a/browser_tests/assets/workflowInMedia/workflow_itxt.png b/browser_tests/assets/workflowInMedia/workflow_itxt.png new file mode 100644 index 0000000000000000000000000000000000000000..95ce757bcedf0c8a9bdf0e4c64aa30da604fdb5c GIT binary patch literal 517 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 FzW^UDpO^pu literal 0 HcmV?d00001 diff --git a/src/scripts/metadata/png.test.ts b/src/scripts/metadata/png.test.ts new file mode 100644 index 000000000..dcc47bf04 --- /dev/null +++ b/src/scripts/metadata/png.test.ts @@ -0,0 +1,245 @@ +import { describe, expect, it } from 'vitest' + +import { getFromPngBuffer } from './png' + +function createPngWithChunk( + chunkType: string, + keyword: string, + content: string, + options: { + compressionFlag?: number + compressionMethod?: number + languageTag?: string + translatedKeyword?: string + } = {} +): ArrayBuffer { + const { + compressionFlag = 0, + compressionMethod = 0, + languageTag = '', + translatedKeyword = '' + } = options + + const signature = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a + ]) + const typeBytes = new TextEncoder().encode(chunkType) + const keywordBytes = new TextEncoder().encode(keyword) + const contentBytes = new TextEncoder().encode(content) + + let chunkData: Uint8Array + if (chunkType === 'iTXt') { + const langBytes = new TextEncoder().encode(languageTag) + const transBytes = new TextEncoder().encode(translatedKeyword) + const totalLength = + keywordBytes.length + + 1 + + 2 + + langBytes.length + + 1 + + transBytes.length + + 1 + + contentBytes.length + + chunkData = new Uint8Array(totalLength) + let pos = 0 + chunkData.set(keywordBytes, pos) + pos += keywordBytes.length + chunkData[pos++] = 0 + chunkData[pos++] = compressionFlag + chunkData[pos++] = compressionMethod + chunkData.set(langBytes, pos) + pos += langBytes.length + chunkData[pos++] = 0 + chunkData.set(transBytes, pos) + pos += transBytes.length + chunkData[pos++] = 0 + chunkData.set(contentBytes, pos) + } else { + chunkData = new Uint8Array(keywordBytes.length + 1 + contentBytes.length) + chunkData.set(keywordBytes, 0) + chunkData[keywordBytes.length] = 0 + chunkData.set(contentBytes, keywordBytes.length + 1) + } + + const lengthBytes = new Uint8Array(4) + new DataView(lengthBytes.buffer).setUint32(0, chunkData.length, false) + + const crc = new Uint8Array(4) + + const iendType = new TextEncoder().encode('IEND') + const iendLength = new Uint8Array(4) + const iendCrc = new Uint8Array(4) + + const total = signature.length + 4 + 4 + chunkData.length + 4 + 4 + 4 + 0 + 4 + const result = new Uint8Array(total) + + let offset = 0 + result.set(signature, offset) + offset += signature.length + + result.set(lengthBytes, offset) + offset += 4 + result.set(typeBytes, offset) + offset += 4 + result.set(chunkData, offset) + offset += chunkData.length + result.set(crc, offset) + offset += 4 + + result.set(iendLength, offset) + offset += 4 + result.set(iendType, offset) + offset += 4 + result.set(iendCrc, offset) + + return result.buffer +} + +describe('getFromPngBuffer', () => { + it('returns empty object for invalid PNG', async () => { + const invalidData = new ArrayBuffer(8) + const result = await getFromPngBuffer(invalidData) + expect(result).toEqual({}) + }) + + it('parses tEXt chunk', async () => { + const workflow = '{"nodes":[]}' + const buffer = createPngWithChunk('tEXt', 'workflow', workflow) + const result = await getFromPngBuffer(buffer) + expect(result['workflow']).toBe(workflow) + }) + + it('parses comf chunk', async () => { + const prompt = '{"1":{"class_type":"Test"}}' + const buffer = createPngWithChunk('comf', 'prompt', prompt) + const result = await getFromPngBuffer(buffer) + expect(result['prompt']).toBe(prompt) + }) + + it('parses uncompressed iTXt chunk', async () => { + const workflow = '{"nodes":[{"id":1}]}' + const buffer = createPngWithChunk('iTXt', 'workflow', workflow, { + compressionFlag: 0, + compressionMethod: 0 + }) + const result = await getFromPngBuffer(buffer) + expect(result['workflow']).toBe(workflow) + }) + + it('parses iTXt chunk with language tag and translated keyword', async () => { + const workflow = '{"test":"value"}' + const buffer = createPngWithChunk('iTXt', 'workflow', workflow, { + compressionFlag: 0, + languageTag: 'en', + translatedKeyword: 'Workflow' + }) + const result = await getFromPngBuffer(buffer) + expect(result['workflow']).toBe(workflow) + }) + + it('parses compressed iTXt chunk', async () => { + const workflow = '{"nodes":[{"id":1,"type":"KSampler"}]}' + const contentBytes = new TextEncoder().encode(workflow) + + const stream = new CompressionStream('deflate') + const writer = stream.writable.getWriter() + await writer.write(contentBytes) + await writer.close() + + const reader = stream.readable.getReader() + const chunks: Uint8Array[] = [] + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + } + const compressedBytes = new Uint8Array( + chunks.reduce((acc, c) => acc + c.length, 0) + ) + let pos = 0 + for (const chunk of chunks) { + compressedBytes.set(chunk, pos) + pos += chunk.length + } + + const buffer = createPngWithCompressedITXt( + 'workflow', + compressedBytes, + '', + '' + ) + const result = await getFromPngBuffer(buffer) + expect(result['workflow']).toBe(workflow) + }) +}) + +function createPngWithCompressedITXt( + keyword: string, + compressedContent: Uint8Array, + languageTag: string, + translatedKeyword: string +): ArrayBuffer { + const signature = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a + ]) + const typeBytes = new TextEncoder().encode('iTXt') + const keywordBytes = new TextEncoder().encode(keyword) + const langBytes = new TextEncoder().encode(languageTag) + const transBytes = new TextEncoder().encode(translatedKeyword) + + const totalLength = + keywordBytes.length + + 1 + + 2 + + langBytes.length + + 1 + + transBytes.length + + 1 + + compressedContent.length + + const chunkData = new Uint8Array(totalLength) + let pos = 0 + chunkData.set(keywordBytes, pos) + pos += keywordBytes.length + chunkData[pos++] = 0 + chunkData[pos++] = 1 + chunkData[pos++] = 0 + chunkData.set(langBytes, pos) + pos += langBytes.length + chunkData[pos++] = 0 + chunkData.set(transBytes, pos) + pos += transBytes.length + chunkData[pos++] = 0 + chunkData.set(compressedContent, pos) + + const lengthBytes = new Uint8Array(4) + new DataView(lengthBytes.buffer).setUint32(0, chunkData.length, false) + + const crc = new Uint8Array(4) + const iendType = new TextEncoder().encode('IEND') + const iendLength = new Uint8Array(4) + const iendCrc = new Uint8Array(4) + + const total = signature.length + 4 + 4 + chunkData.length + 4 + 4 + 4 + 0 + 4 + const result = new Uint8Array(total) + + let offset = 0 + result.set(signature, offset) + offset += signature.length + result.set(lengthBytes, offset) + offset += 4 + result.set(typeBytes, offset) + offset += 4 + result.set(chunkData, offset) + offset += chunkData.length + result.set(crc, offset) + offset += 4 + result.set(iendLength, offset) + offset += 4 + result.set(iendType, offset) + offset += 4 + result.set(iendCrc, offset) + + return result.buffer +} diff --git a/src/scripts/metadata/png.ts b/src/scripts/metadata/png.ts index 2e911796a..526d9b2d7 100644 --- a/src/scripts/metadata/png.ts +++ b/src/scripts/metadata/png.ts @@ -1,26 +1,59 @@ +async function decompressZlib( + data: Uint8Array +): Promise> { + const stream = new DecompressionStream('deflate') + const writer = stream.writable.getWriter() + try { + await writer.write(data) + await writer.close() + } finally { + writer.releaseLock() + } + + const reader = stream.readable.getReader() + const chunks: Uint8Array[] = [] + let totalLength = 0 + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + totalLength += value.length + } + } finally { + reader.releaseLock() + } + + const result = new Uint8Array(totalLength) + let offset = 0 + for (const chunk of chunks) { + result.set(chunk, offset) + offset += chunk.length + } + return result +} + /** @knipIgnoreUnusedButUsedByCustomNodes */ -export function getFromPngBuffer(buffer: ArrayBuffer): Record { - // Get the PNG data as a Uint8Array +export async function getFromPngBuffer( + buffer: ArrayBuffer +): Promise> { const pngData = new Uint8Array(buffer) const dataView = new DataView(pngData.buffer) - // Check that the PNG signature is present if (dataView.getUint32(0) !== 0x89504e47) { console.error('Not a valid PNG file') return {} } - // Start searching for chunks after the PNG signature let offset = 8 - let txt_chunks: Record = {} - // Loop through the chunks in the PNG file + const txt_chunks: Record = {} + while (offset < pngData.length) { - // Get the length of the chunk const length = dataView.getUint32(offset) - // Get the chunk type const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8)) - if (type === 'tEXt' || type == 'comf' || type === 'iTXt') { - // Get the keyword + + if (type === 'tEXt' || type === 'comf' || type === 'iTXt') { let keyword_end = offset + 8 while (pngData[keyword_end] !== 0) { keyword_end++ @@ -28,11 +61,48 @@ export function getFromPngBuffer(buffer: ArrayBuffer): Record { const keyword = String.fromCharCode( ...pngData.slice(offset + 8, keyword_end) ) - // Get the text - const contentArraySegment = pngData.slice( - keyword_end + 1, - offset + 8 + length - ) + + let textStart = keyword_end + 1 + let isCompressed = false + let compressionMethod = 0 + + if (type === 'iTXt') { + const chunkEnd = offset + 8 + length + isCompressed = pngData[textStart] === 1 + compressionMethod = pngData[textStart + 1] + textStart += 2 + + while (pngData[textStart] !== 0 && textStart < chunkEnd) { + textStart++ + } + if (textStart < chunkEnd) textStart++ + + while (pngData[textStart] !== 0 && textStart < chunkEnd) { + textStart++ + } + if (textStart < chunkEnd) textStart++ + } + + let contentArraySegment = pngData.slice(textStart, offset + 8 + length) + + if (isCompressed) { + if (compressionMethod === 0) { + try { + contentArraySegment = await decompressZlib(contentArraySegment) + } catch (e) { + console.error(`Failed to decompress iTXt chunk "${keyword}":`, e) + offset += 12 + length + continue + } + } else { + console.warn( + `Unsupported compression method ${compressionMethod} for iTXt chunk "${keyword}"` + ) + offset += 12 + length + continue + } + } + const contentJson = new TextDecoder('utf-8').decode(contentArraySegment) txt_chunks[keyword] = contentJson } @@ -42,14 +112,21 @@ export function getFromPngBuffer(buffer: ArrayBuffer): Record { return txt_chunks } -export function getFromPngFile(file: File) { - return new Promise>((r) => { +export async function getFromPngFile( + file: File +): Promise> { + return new Promise>((resolve, reject) => { const reader = new FileReader() - reader.onload = (event) => { - // @ts-expect-error fixme ts strict error - r(getFromPngBuffer(event.target.result as ArrayBuffer)) + reader.onload = async (event) => { + const buffer = event.target?.result + if (!(buffer instanceof ArrayBuffer)) { + reject(new Error('Failed to read file as ArrayBuffer')) + return + } + const result = await getFromPngBuffer(buffer) + resolve(result) } - + reader.onerror = () => reject(reader.error) reader.readAsArrayBuffer(file) }) }