mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 09:18:26 +00:00
Compare commits
3 Commits
codex/rum-
...
matt/be-22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5af924ba9 | ||
|
|
13cd0420f4 | ||
|
|
d1f7a71809 |
@@ -77,11 +77,14 @@ describe('AVIF metadata', () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockFileReaderError('readAsArrayBuffer')
|
||||
expect(await getFromAvifFile(file)).toEqual({})
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resolves empty when the FileReader fires abort', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockFileReaderAbort('readAsArrayBuffer')
|
||||
expect(await getFromAvifFile(file)).toEqual({})
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from '@/types/metadataTypes'
|
||||
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
|
||||
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
const readNullTerminatedString = (
|
||||
dataView: DataView,
|
||||
start: number,
|
||||
@@ -388,36 +390,24 @@ function parseExifData(exifData) {
|
||||
return ifdData
|
||||
}
|
||||
|
||||
export function getFromAvifFile(file: File): Promise<Record<string, string>> {
|
||||
return new Promise<Record<string, string>>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const buffer = event.target?.result as ArrayBuffer
|
||||
if (!buffer) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
export async function getFromAvifFile(
|
||||
file: File
|
||||
): Promise<Record<string, string>> {
|
||||
const buffer = await readFileAsArrayBuffer(file)
|
||||
if (!buffer) return {}
|
||||
|
||||
try {
|
||||
const comfyMetadata = parseAvifMetadata(buffer)
|
||||
const result: Record<string, string> = {}
|
||||
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({})
|
||||
}
|
||||
try {
|
||||
const comfyMetadata = parseAvifMetadata(buffer)
|
||||
const result: Record<string, string> = {}
|
||||
if (comfyMetadata.prompt) {
|
||||
result.prompt = JSON.stringify(comfyMetadata.prompt)
|
||||
}
|
||||
reader.onerror = (err) => {
|
||||
console.error('FileReader: Error reading AVIF file:', err)
|
||||
resolve({})
|
||||
if (comfyMetadata.workflow) {
|
||||
result.workflow = JSON.stringify(comfyMetadata.workflow)
|
||||
}
|
||||
reader.onabort = () => resolve({})
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('Parser: Error parsing AVIF metadata:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from '@/types/metadataTypes'
|
||||
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
|
||||
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
const WEBM_SIGNATURE = [0x1a, 0x45, 0xdf, 0xa3]
|
||||
const MAX_READ_BYTES = 2 * 1024 * 1024
|
||||
const EBML_ID = {
|
||||
@@ -325,25 +327,14 @@ const parseMetadata = (data: Uint8Array): ComfyMetadata => {
|
||||
return meta
|
||||
}
|
||||
|
||||
const handleFileLoad = (
|
||||
event: ProgressEvent<FileReader>,
|
||||
resolve: (value: ComfyMetadata) => void
|
||||
) => {
|
||||
if (!event.target?.result) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
const parseWebmBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
|
||||
try {
|
||||
const data = new Uint8Array(event.target.result as ArrayBuffer)
|
||||
if (data.length < 4 || !hasWebmSignature(data)) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
const data = new Uint8Array(buffer)
|
||||
if (data.length < 4 || !hasWebmSignature(data)) return {}
|
||||
|
||||
resolve(parseMetadata(data))
|
||||
return parseMetadata(data)
|
||||
} catch {
|
||||
resolve({})
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,12 +342,9 @@ const handleFileLoad = (
|
||||
* Extracts ComfyUI Workflow metadata from a WebM file
|
||||
* @param file - The WebM file to extract metadata from
|
||||
*/
|
||||
export function getFromWebmFile(file: File): Promise<ComfyMetadata> {
|
||||
return new Promise<ComfyMetadata>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => handleFileLoad(event, resolve)
|
||||
reader.onerror = () => resolve({})
|
||||
reader.onabort = () => resolve({})
|
||||
reader.readAsArrayBuffer(file.slice(0, MAX_READ_BYTES))
|
||||
})
|
||||
export async function getFromWebmFile(file: File): Promise<ComfyMetadata> {
|
||||
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
|
||||
if (!buffer) return {}
|
||||
|
||||
return parseWebmBuffer(buffer)
|
||||
}
|
||||
|
||||
@@ -28,10 +28,24 @@ describe('FLAC metadata', () => {
|
||||
expect(result.prompt).toBe(JSON.stringify(EXPECTED_PROMPT))
|
||||
})
|
||||
|
||||
it('returns undefined for non-FLAC data', () => {
|
||||
it('returns empty and logs for non-FLAC data', () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const buf = new ArrayBuffer(16)
|
||||
|
||||
const result = getFromFlacBuffer(buf)
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(console.error).toHaveBeenCalledWith('Not a valid FLAC file')
|
||||
})
|
||||
|
||||
it('returns empty for a FLAC without a Vorbis Comment block', () => {
|
||||
const buffer = new Uint8Array([
|
||||
0x66, 0x4c, 0x61, 0x43, 0x80, 0x00, 0x00, 0x00
|
||||
]).buffer
|
||||
|
||||
const result = getFromFlacBuffer(buffer)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
describe('FileReader failure modes', () => {
|
||||
@@ -53,4 +67,18 @@ describe('FLAC metadata', () => {
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves empty when parsing throws on malformed data', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const malformed = new Uint8Array([0x66, 0x4c, 0x61, 0x43, 0xff, 0xff])
|
||||
const file = new File([malformed], 'malformed.flac')
|
||||
|
||||
const result = await getFromFlacFile(file)
|
||||
|
||||
expect(result).toEqual({})
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Parser: Error parsing FLAC metadata:',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
const dataView = new DataView(buffer)
|
||||
|
||||
@@ -5,8 +7,7 @@ export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
const signature = String.fromCharCode(...new Uint8Array(buffer, 0, 4))
|
||||
if (signature !== 'fLaC') {
|
||||
console.error('Not a valid FLAC file')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return
|
||||
return {}
|
||||
}
|
||||
|
||||
// Parse metadata blocks
|
||||
@@ -29,22 +30,21 @@ export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
|
||||
if (isLastBlock) break
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return vorbisComment
|
||||
return vorbisComment ?? {}
|
||||
}
|
||||
|
||||
export function getFromFlacFile(file: File): Promise<Record<string, string>> {
|
||||
return new Promise((r) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const arrayBuffer = event.target.result as ArrayBuffer
|
||||
r(getFromFlacBuffer(arrayBuffer))
|
||||
}
|
||||
reader.onerror = () => r({})
|
||||
reader.onabort = () => r({})
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
export async function getFromFlacFile(
|
||||
file: File
|
||||
): Promise<Record<string, string>> {
|
||||
const buffer = await readFileAsArrayBuffer(file)
|
||||
if (!buffer) return {}
|
||||
|
||||
try {
|
||||
return getFromFlacBuffer(buffer)
|
||||
} catch (e) {
|
||||
console.error('Parser: Error parsing FLAC metadata:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to parse the Vorbis Comment block
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from '@/types/metadataTypes'
|
||||
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
|
||||
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
const MAX_READ_BYTES = 1 << 20
|
||||
|
||||
const isJsonChunk = (chunk: GltfChunkHeader | null): boolean =>
|
||||
@@ -141,27 +143,15 @@ const processGltfFileBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
|
||||
/**
|
||||
* Extract ComfyUI metadata from a GLTF binary file (GLB)
|
||||
*/
|
||||
export function getGltfBinaryMetadata(file: File): Promise<ComfyMetadata> {
|
||||
return new Promise<ComfyMetadata>((resolve) => {
|
||||
if (!file) return Promise.resolve({})
|
||||
export async function getGltfBinaryMetadata(
|
||||
file: File
|
||||
): Promise<ComfyMetadata> {
|
||||
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
|
||||
if (!buffer) return {}
|
||||
|
||||
const bytesToRead = Math.min(file.size, MAX_READ_BYTES)
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
if (!event.target?.result) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(processGltfFileBuffer(event.target.result as ArrayBuffer))
|
||||
} catch {
|
||||
resolve({})
|
||||
}
|
||||
}
|
||||
reader.onerror = () => resolve({})
|
||||
reader.onabort = () => resolve({})
|
||||
reader.readAsArrayBuffer(file.slice(0, bytesToRead))
|
||||
})
|
||||
try {
|
||||
return processGltfFileBuffer(buffer)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,14 @@ describe('ISOBMFF (MP4) metadata', () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockFileReaderError('readAsArrayBuffer')
|
||||
expect(await getFromIsobmffFile(file)).toEqual({})
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resolves empty when the FileReader fires abort', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockFileReaderAbort('readAsArrayBuffer')
|
||||
expect(await getFromIsobmffFile(file)).toEqual({})
|
||||
expect(console.error).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '@/types/metadataTypes'
|
||||
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
|
||||
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
// Set max read high, as atoms are stored near end of file
|
||||
// while search is made to be efficient.
|
||||
const MAX_READ_BYTES = 64 * 1024 * 1024
|
||||
@@ -256,28 +258,14 @@ const parseIsobmffMetadata = (data: Uint8Array): ComfyMetadata => {
|
||||
* (e.g., MP4, MOV) by parsing the `udta.meta.keys` and `udta.meta.ilst` boxes.
|
||||
* @param file - The file to extract metadata from.
|
||||
*/
|
||||
export function getFromIsobmffFile(file: File): Promise<ComfyMetadata> {
|
||||
return new Promise<ComfyMetadata>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event: ProgressEvent<FileReader>) => {
|
||||
if (!event.target?.result) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
export async function getFromIsobmffFile(file: File): Promise<ComfyMetadata> {
|
||||
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
|
||||
if (!buffer) return {}
|
||||
|
||||
try {
|
||||
const data = new Uint8Array(event.target.result as ArrayBuffer)
|
||||
resolve(parseIsobmffMetadata(data))
|
||||
} catch (e) {
|
||||
console.error('Parser: Error parsing ISOBMFF metadata:', e)
|
||||
resolve({})
|
||||
}
|
||||
}
|
||||
reader.onerror = (err) => {
|
||||
console.error('FileReader: Error reading ISOBMFF file:', err)
|
||||
resolve({})
|
||||
}
|
||||
reader.onabort = () => resolve({})
|
||||
reader.readAsArrayBuffer(file.slice(0, MAX_READ_BYTES))
|
||||
})
|
||||
try {
|
||||
return parseIsobmffMetadata(new Uint8Array(buffer))
|
||||
} catch (e) {
|
||||
console.error('Parser: Error parsing ISOBMFF metadata:', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
50
src/scripts/metadata/readFile.test.ts
Normal file
50
src/scripts/metadata/readFile.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
mockFileReaderAbort,
|
||||
mockFileReaderError
|
||||
} from './__fixtures__/helpers'
|
||||
import { readFileAsArrayBuffer } from './readFile'
|
||||
|
||||
describe('readFileAsArrayBuffer', () => {
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('reads the whole file into an ArrayBuffer when no cap is given', async () => {
|
||||
const bytes = new Uint8Array([1, 2, 3, 4, 5])
|
||||
const file = new File([bytes], 'test.bin')
|
||||
|
||||
const buffer = await readFileAsArrayBuffer(file)
|
||||
|
||||
expect(buffer).toBeInstanceOf(ArrayBuffer)
|
||||
expect(new Uint8Array(buffer!)).toEqual(bytes)
|
||||
})
|
||||
|
||||
it('reads only the first maxBytes when a cap is given', async () => {
|
||||
const file = new File([new Uint8Array(100)], 'test.bin')
|
||||
|
||||
const buffer = await readFileAsArrayBuffer(file, 10)
|
||||
|
||||
expect(buffer?.byteLength).toBe(10)
|
||||
})
|
||||
|
||||
it('returns an empty ArrayBuffer (not null) when maxBytes is 0', async () => {
|
||||
const file = new File([new Uint8Array(10)], 'test.bin')
|
||||
|
||||
const buffer = await readFileAsArrayBuffer(file, 0)
|
||||
|
||||
expect(buffer).toBeInstanceOf(ArrayBuffer)
|
||||
expect(buffer?.byteLength).toBe(0)
|
||||
})
|
||||
|
||||
it('resolves null when the read fires an error', async () => {
|
||||
mockFileReaderError('readAsArrayBuffer')
|
||||
|
||||
expect(await readFileAsArrayBuffer(new File([], 'test.bin'))).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves null when the read is aborted', async () => {
|
||||
mockFileReaderAbort('readAsArrayBuffer')
|
||||
|
||||
expect(await readFileAsArrayBuffer(new File([], 'test.bin'))).toBeNull()
|
||||
})
|
||||
})
|
||||
24
src/scripts/metadata/readFile.ts
Normal file
24
src/scripts/metadata/readFile.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Reads a file (optionally capped to its first `maxBytes`) into an ArrayBuffer.
|
||||
* Resolves `null` when the read errors, aborts, or yields no result, so callers
|
||||
* can guard with a single truthiness check before parsing. The underlying
|
||||
* `FileReader` error reason is intentionally not surfaced; callers treat any
|
||||
* failure as "no metadata" rather than distinguishing causes.
|
||||
* @param file - The file to read.
|
||||
* @param maxBytes - Optional cap; when set, only the first `maxBytes` are read.
|
||||
*/
|
||||
export function readFileAsArrayBuffer(
|
||||
file: File,
|
||||
maxBytes?: number
|
||||
): Promise<ArrayBuffer | null> {
|
||||
return new Promise<ArrayBuffer | null>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve(reader.result instanceof ArrayBuffer ? reader.result : null)
|
||||
}
|
||||
reader.onerror = () => resolve(null)
|
||||
reader.onabort = () => resolve(null)
|
||||
const blob = maxBytes === undefined ? file : file.slice(0, maxBytes)
|
||||
reader.readAsArrayBuffer(blob)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user