From 37bfc5361685b46fca4402fa22d93f70d5b33759 Mon Sep 17 00:00:00 2001 From: Ferrah Aiko Date: Thu, 24 Jul 2025 03:20:39 -0300 Subject: [PATCH] Add the ability to parse workflows from AVIF images (#4420) --- browser_tests/assets/workflow.avif | Bin 0 -> 25985 bytes .../tests/loadWorkflowInMedia.spec.ts | 3 +- src/constants/supportedWorkflowFormats.ts | 4 +- src/scripts/app.ts | 11 + src/scripts/metadata/avif.ts | 412 ++++++++++++++++++ src/scripts/pnginfo.ts | 5 + src/types/metadataTypes.ts | 54 +++ 7 files changed, 486 insertions(+), 3 deletions(-) create mode 100644 browser_tests/assets/workflow.avif create mode 100644 src/scripts/metadata/avif.ts diff --git a/browser_tests/assets/workflow.avif b/browser_tests/assets/workflow.avif new file mode 100644 index 0000000000000000000000000000000000000000..8d8d77cede9862a2bce6464328b7074827c72f95 GIT binary patch literal 25985 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 literal 0 HcmV?d00001 diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 215f977e7..1250d3385 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -15,7 +15,8 @@ test.describe('Load Workflow in Media', () => { 'workflow.mp4', 'workflow.mov', 'workflow.m4v', - 'workflow.svg' + 'workflow.svg', + 'workflow.avif' ] fileNames.forEach(async (fileName) => { test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ diff --git a/src/constants/supportedWorkflowFormats.ts b/src/constants/supportedWorkflowFormats.ts index f00bd3f05..ac0bee186 100644 --- a/src/constants/supportedWorkflowFormats.ts +++ b/src/constants/supportedWorkflowFormats.ts @@ -6,8 +6,8 @@ * All supported image formats that can contain workflow data */ export const IMAGE_WORKFLOW_FORMATS = { - extensions: ['.png', '.webp', '.svg'], - mimeTypes: ['image/png', 'image/webp', 'image/svg+xml'] + extensions: ['.png', '.webp', '.svg', '.avif'], + mimeTypes: ['image/png', 'image/webp', 'image/svg+xml', 'image/avif'] } /** diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 2061a5f6a..257a30cf6 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -80,6 +80,7 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard' import { type ComfyApi, PromptExecutionError, api } from './api' import { defaultGraph } from './defaultGraph' import { + getAvifMetadata, getFlacMetadata, getLatentMetadata, getPngMetadata, @@ -1351,6 +1352,16 @@ export class ComfyApp { } else { this.showErrorOnFileLoad(file) } + } else if (file.type === 'image/avif') { + const { workflow, prompt } = await getAvifMetadata(file) + + if (workflow) { + this.loadGraphData(JSON.parse(workflow), true, true, fileName) + } else if (prompt) { + this.loadApiJson(JSON.parse(prompt), fileName) + } else { + this.showErrorOnFileLoad(file) + } } else if (file.type === 'image/webp') { const pngInfo = await getWebpMetadata(file) // Support loading workflows from that webp custom node. diff --git a/src/scripts/metadata/avif.ts b/src/scripts/metadata/avif.ts new file mode 100644 index 000000000..d23adf4bb --- /dev/null +++ b/src/scripts/metadata/avif.ts @@ -0,0 +1,412 @@ +import { + type AvifIinfBox, + type AvifIlocBox, + type AvifInfeBox, + ComfyMetadata, + ComfyMetadataTags, + type IsobmffBoxContentRange +} from '@/types/metadataTypes' + +const readNullTerminatedString = ( + dataView: DataView, + start: number, + end: number +): { str: string; length: number } => { + let length = 0 + while (start + length < end && dataView.getUint8(start + length) !== 0) { + length++ + } + const str = new TextDecoder('utf-8').decode( + new Uint8Array(dataView.buffer, dataView.byteOffset + start, length) + ) + return { str, length: length + 1 } // Include null terminator +} + +const parseInfeBox = (dataView: DataView, start: number): AvifInfeBox => { + const version = dataView.getUint8(start) + const flags = dataView.getUint32(start) & 0xffffff + let offset = start + 4 + + let item_ID: number, item_protection_index: number, item_type: string + + if (version >= 2) { + if (version === 2) { + item_ID = dataView.getUint16(offset) + offset += 2 + } else { + item_ID = dataView.getUint32(offset) + offset += 4 + } + + item_protection_index = dataView.getUint16(offset) + offset += 2 + item_type = String.fromCharCode( + ...new Uint8Array(dataView.buffer, dataView.byteOffset + offset, 4) + ) + offset += 4 + + const { str: item_name, length: name_len } = readNullTerminatedString( + dataView, + offset, + dataView.byteLength + ) + offset += name_len + + const content_type = readNullTerminatedString( + dataView, + offset, + dataView.byteLength + ).str + + return { + box_header: { size: 0, type: 'infe' }, // Size is dynamic + version, + flags, + item_ID, + item_protection_index, + item_type, + item_name, + content_type + } + } + throw new Error(`Unsupported infe box version: ${version}`) +} + +const parseIinfBox = ( + dataView: DataView, + range: IsobmffBoxContentRange +): AvifIinfBox => { + if (!range) throw new Error('iinf box not found') + + const version = dataView.getUint8(range.start) + const flags = dataView.getUint32(range.start) & 0xffffff + let offset = range.start + 4 + + const entry_count = + version === 0 ? dataView.getUint16(offset) : dataView.getUint32(offset) + offset += version === 0 ? 2 : 4 + + const entries: AvifInfeBox[] = [] + for (let i = 0; i < entry_count; i++) { + const boxSize = dataView.getUint32(offset) + const boxType = String.fromCharCode( + ...new Uint8Array(dataView.buffer, dataView.byteOffset + offset + 4, 4) + ) + + if (boxType === 'infe') { + const infe = parseInfeBox(dataView, offset + 8) + infe.box_header.size = boxSize + entries.push(infe) + } + offset += boxSize + } + + return { + box_header: { size: range.end - range.start + 8, type: 'iinf' }, + version, + flags, + entry_count, + entries + } +} + +const parseIlocBox = ( + dataView: DataView, + range: IsobmffBoxContentRange +): AvifIlocBox => { + if (!range) throw new Error('iloc box not found') + + const version = dataView.getUint8(range.start) + const flags = dataView.getUint32(range.start) & 0xffffff + let offset = range.start + 4 + + const sizes = dataView.getUint8(offset++) + const offset_size = (sizes >> 4) & 0x0f + const length_size = sizes & 0x0f + + const base_offset_size = (dataView.getUint8(offset) >> 4) & 0x0f + const index_size = + version === 1 || version === 2 ? dataView.getUint8(offset) & 0x0f : 0 + offset++ + + const item_count = + version < 2 ? dataView.getUint16(offset) : dataView.getUint32(offset) + offset += version < 2 ? 2 : 4 + + const items = [] + for (let i = 0; i < item_count; i++) { + const item_ID = + version < 2 ? dataView.getUint16(offset) : dataView.getUint32(offset) + offset += version < 2 ? 2 : 4 + + if (version === 1 || version === 2) { + offset += 2 // construction_method + } + + const data_reference_index = dataView.getUint16(offset) + offset += 2 + + const base_offset = base_offset_size > 0 ? dataView.getUint32(offset) : 0 // Simplified + offset += base_offset_size + + const extent_count = dataView.getUint16(offset) + offset += 2 + + const extents = [] + for (let j = 0; j < extent_count; j++) { + if ((version === 1 || version === 2) && index_size > 0) { + offset += index_size + } + const extent_offset = dataView.getUint32(offset) // Simplified + offset += offset_size + const extent_length = dataView.getUint32(offset) // Simplified + offset += length_size + extents.push({ extent_offset, extent_length }) + } + items.push({ + item_ID, + data_reference_index, + base_offset, + extent_count, + extents + }) + } + + return { + box_header: { size: range.end - range.start + 8, type: 'iloc' }, + version, + flags, + offset_size, + length_size, + base_offset_size, + index_size, + item_count, + items + } +} + +function findBox( + dataView: DataView, + start: number, + end: number, + type: string +): IsobmffBoxContentRange { + let offset = start + while (offset < end) { + if (offset + 8 > end) break + const boxLength = dataView.getUint32(offset) + const boxType = String.fromCharCode( + ...new Uint8Array(dataView.buffer, dataView.byteOffset + offset + 4, 4) + ) + + if (boxLength === 0) break + + if (boxType === type) { + return { start: offset + 8, end: offset + boxLength } + } + if (offset + boxLength > end) break + offset += boxLength + } + return null +} + +function parseAvifMetadata(buffer: ArrayBuffer): ComfyMetadata { + const metadata: ComfyMetadata = {} + const dataView = new DataView(buffer) + + if ( + dataView.getUint32(4) !== 0x66747970 || + dataView.getUint32(8) !== 0x61766966 + ) { + console.error('Not a valid AVIF file') + return {} + } + + const metaBox = findBox(dataView, 0, dataView.byteLength, 'meta') + if (!metaBox) return {} + + const metaBoxContentStart = metaBox.start + 4 // Skip version and flags + + const iinfBoxRange = findBox( + dataView, + metaBoxContentStart, + metaBox.end, + 'iinf' + ) + const iinf = parseIinfBox(dataView, iinfBoxRange) + + const exifInfe = iinf.entries.find((e) => e.item_type === 'Exif') + if (!exifInfe) return {} + + const ilocBoxRange = findBox( + dataView, + metaBoxContentStart, + metaBox.end, + 'iloc' + ) + const iloc = parseIlocBox(dataView, ilocBoxRange) + + const exifIloc = iloc.items.find((i) => i.item_ID === exifInfe.item_ID) + if (!exifIloc || exifIloc.extents.length === 0) return {} + + const exifExtent = exifIloc.extents[0] + const itemData = new Uint8Array( + buffer, + exifExtent.extent_offset, + exifExtent.extent_length + ) + + let tiffHeaderOffset = -1 + for (let i = 0; i < itemData.length - 4; i++) { + if ( + (itemData[i] === 0x4d && + itemData[i + 1] === 0x4d && + itemData[i + 2] === 0x00 && + itemData[i + 3] === 0x2a) || // MM* + (itemData[i] === 0x49 && + itemData[i + 1] === 0x49 && + itemData[i + 2] === 0x2a && + itemData[i + 3] === 0x00) // II* + ) { + tiffHeaderOffset = i + break + } + } + + if (tiffHeaderOffset !== -1) { + const exifData = itemData.subarray(tiffHeaderOffset) + const data: Record = parseExifData(exifData) + for (const key in data) { + const value = data[key] + if (typeof value === 'string') { + if (key === 'usercomment') { + try { + const metadataJson = JSON.parse(value) + if (metadataJson.prompt) { + metadata[ComfyMetadataTags.PROMPT] = metadataJson.prompt + } + if (metadataJson.workflow) { + metadata[ComfyMetadataTags.WORKFLOW] = metadataJson.workflow + } + } catch (e) { + console.error('Failed to parse usercomment JSON', e) + } + } else { + const [metadataKey, ...metadataValueParts] = value.split(':') + const metadataValue = metadataValueParts.join(':').trim() + if ( + metadataKey.toLowerCase() === + ComfyMetadataTags.PROMPT.toLowerCase() || + metadataKey.toLowerCase() === + ComfyMetadataTags.WORKFLOW.toLowerCase() + ) { + try { + const jsonValue = JSON.parse(metadataValue) + metadata[metadataKey.toLowerCase() as keyof ComfyMetadata] = + jsonValue + } catch (e) { + console.error(`Failed to parse JSON for ${metadataKey}`, e) + } + } + } + } + } + } else { + console.log('Warning: TIFF header not found in EXIF data.') + } + + return metadata +} + +// @ts-expect-error fixme ts strict error +export function parseExifData(exifData) { + // Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian) + const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II' + + // Function to read 16-bit and 32-bit integers from binary data + // @ts-expect-error fixme ts strict error + function readInt(offset, isLittleEndian, length) { + let arr = exifData.slice(offset, offset + length) + if (length === 2) { + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16( + 0, + isLittleEndian + ) + } else if (length === 4) { + return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32( + 0, + isLittleEndian + ) + } + } + + // Read the offset to the first IFD (Image File Directory) + const ifdOffset = readInt(4, isLittleEndian, 4) + + // @ts-expect-error fixme ts strict error + function parseIFD(offset) { + const numEntries = readInt(offset, isLittleEndian, 2) + const result = {} + + // @ts-expect-error fixme ts strict error + for (let i = 0; i < numEntries; i++) { + const entryOffset = offset + 2 + i * 12 + const tag = readInt(entryOffset, isLittleEndian, 2) + const type = readInt(entryOffset + 2, isLittleEndian, 2) + const numValues = readInt(entryOffset + 4, isLittleEndian, 4) + const valueOffset = readInt(entryOffset + 8, isLittleEndian, 4) + + // Read the value(s) based on the data type + let value + if (type === 2) { + // ASCII string + value = new TextDecoder('utf-8').decode( + // @ts-expect-error fixme ts strict error + exifData.subarray(valueOffset, valueOffset + numValues - 1) + ) + } + + // @ts-expect-error fixme ts strict error + result[tag] = value + } + + return result + } + + // Parse the first IFD + const ifdData = parseIFD(ifdOffset) + return ifdData +} + +export function getFromAvifFile(file: File): Promise> { + return new Promise>((resolve) => { + const reader = new FileReader() + reader.onload = (event) => { + const buffer = event.target?.result as ArrayBuffer + if (!buffer) { + resolve({}) + return + } + + try { + const comfyMetadata = parseAvifMetadata(buffer) + const result: Record = {} + if (comfyMetadata.prompt) { + result.prompt = JSON.stringify(comfyMetadata.prompt) + } + if (comfyMetadata.workflow) { + result.workflow = JSON.stringify(comfyMetadata.workflow) + } + resolve(result) + } catch (e) { + console.error('Parser: Error parsing AVIF metadata:', e) + resolve({}) + } + } + reader.onerror = (err) => { + console.error('FileReader: Error reading AVIF file:', err) + resolve({}) + } + reader.readAsArrayBuffer(file) + }) +} diff --git a/src/scripts/pnginfo.ts b/src/scripts/pnginfo.ts index 7c7f6f597..689537f7e 100644 --- a/src/scripts/pnginfo.ts +++ b/src/scripts/pnginfo.ts @@ -1,6 +1,7 @@ import { LiteGraph } from '@comfyorg/litegraph' import { api } from './api' +import { getFromAvifFile } from './metadata/avif' import { getFromFlacFile } from './metadata/flac' import { getFromPngFile } from './metadata/png' @@ -13,6 +14,10 @@ export function getFlacMetadata(file: File): Promise> { return getFromFlacFile(file) } +export function getAvifMetadata(file: File): Promise> { + return getFromAvifFile(file) +} + // @ts-expect-error fixme ts strict error function parseExifData(exifData) { // Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian) diff --git a/src/types/metadataTypes.ts b/src/types/metadataTypes.ts index 9170ef6d4..c63324196 100644 --- a/src/types/metadataTypes.ts +++ b/src/types/metadataTypes.ts @@ -85,3 +85,57 @@ export type GltfJsonData = { * Null if the box was not found. */ export type IsobmffBoxContentRange = { start: number; end: number } | null + +export type AvifInfeBox = { + box_header: { + size: number + type: 'infe' + } + version: number + flags: number + item_ID: number + item_protection_index: number + item_type: string + item_name: string + content_type?: string + content_encoding?: string +} + +export type AvifIinfBox = { + box_header: { + size: number + type: 'iinf' + } + version: number + flags: number + entry_count: number + entries: AvifInfeBox[] +} + +export type AvifIlocItemExtent = { + extent_offset: number + extent_length: number +} + +export type AvifIlocItem = { + item_ID: number + data_reference_index: number + base_offset: number + extent_count: number + extents: AvifIlocItemExtent[] +} + +export type AvifIlocBox = { + box_header: { + size: number + type: 'iloc' + } + version: number + flags: number + offset_size: number + length_size: number + base_offset_size: number + index_size: number + item_count: number + items: AvifIlocItem[] +}