Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add avif parsing function #3565

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion web/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ComfyWidgets, initWidgets } from "./widgets.js";
import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, getWebpMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import { getPngMetadata, getWebpMetadata, getAvifMetadata, importA1111, getLatentMetadata } from "./pnginfo.js";
import { addDomClippingSetting } from "./domWidget.js";
import { createImageHost, calculateImageGrid } from "./ui/imagePreview.js"

Expand Down Expand Up @@ -2187,6 +2187,19 @@ export class ComfyApp {
const workflow = pngInfo?.workflow || pngInfo?.Workflow;
const prompt = pngInfo?.prompt || pngInfo?.Prompt;

if (workflow) {
this.loadGraphData(JSON.parse(workflow));
} else if (prompt) {
this.loadApiJson(JSON.parse(prompt));
} else {
this.showErrorOnFileLoad(file);
}
} else if (file.type === "image/avif") {
const pngInfo = await getAvifMetadata(file);
// Support loading workflows from that webp custom node.
const workflow = pngInfo?.workflow || pngInfo?.Workflow;
const prompt = pngInfo?.prompt || pngInfo?.Prompt;

if (workflow) {
this.loadGraphData(JSON.parse(workflow));
} else if (prompt) {
Expand Down
148 changes: 145 additions & 3 deletions web/scripts/pnginfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ export function getWebpMetadata(file) {
}
let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length));
for (var key in data) {
var value = data[key];
let index = value.indexOf(':');
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
if (data[key]) {
var value = data[key];
let index = value.indexOf(':');
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
}
}
}

Expand All @@ -146,6 +148,146 @@ export function getWebpMetadata(file) {
});
}


export function getAvifMetadata(file) {
return new Promise((r) => {
const reader = new FileReader();
reader.onload = (event) => {
const avif = new Uint8Array(event.target.result);
const dataView = new DataView(avif.buffer);

// https://aomediacodec.github.io/av1-avif/#brands-overview
// https://www.garykessler.net/library/file_sigs.html
// Check that the AVIF signature is present: [4 byte offset] ftypheic or ftypavif
// console.log('avif dataView.getUint32(4)',dataView.getUint32(4)); // 1718909296 = 0x66747970 = ftyp
// console.log('avif dataView.getUint32(8)',dataView.getUint32(8)); // 1635150182 = 0x61766966 = avif
// console.log('avif dataView.getUint32(8)',dataView.getUint32(8)); // 1751476579 = 0x68656963 = heic
// 1718909296 = 0x66747970 = ftyp 1635150182 = 0x61766966 = avif 1751476579 = 0x68656963 = heic
if (!(dataView.getUint32(4) == 0x66747970 && (dataView.getUint32(8) == 0x61766966 || dataView.getUint32(8) == 0x68656963))) {
console.error("Not a valid AVIF file");
r();
return;
}

// Start searching for Exif chunks after the AVIF signature
/*
we have in this order:
- signature that starts at 0x4 and is 8-byte long: ftypavif most likely
- 4-byte 00 space
- multiple 4-byte words defining how the avif was build, ending with 00 00
- examples above have avifmiflmiaf or avifmiflmiafMA1B
- Then the core image definition length on 2-bytes: examples above are 0x133 nd 0x12B long, ending just before the 8-byte mdat section
- mdat section is 8-byte long
- then we finally have the 6-byte Exif\0\0 followed by the IFD definitions, and then the Exif chunks

we start at offset 4 + 8 + 4 = 0x10
then we slice n 4-byte words until the last one starts with 00 00: `0x10 + 0x4n` = offset for core metadata size (meta)
then we read core metadata size offset as a 4-byte word (meta)
then we add `0x10 + 0x4n + 4 + meta + 8` = offset for Exif\0\0 section!
*/
let metaOffset = 0x10
while (metaOffset < avif.length / 2) {
let word = String.fromCharCode(...avif.slice(metaOffset, metaOffset+4));
if (word.slice(0,2) != "\0\0") {
metaOffset += 4
} else break;
}
let metaSize = dataView.getUint32(metaOffset);
let offset = metaOffset + 4 + metaSize + 8;

// Now we calculate offsetChunk_length = offset for the Exif chunk size
/*
We start from metaOffset + 4 = offset for the whole meta section. As seen above, each section length in meta is defined by the last 1 or 2-byte, more or less.
we set slice = 0xC
as long as the current 4-byte word is not == iloc:
we slice and get the last 2 as length for the next section;
we offset + slice and
if next 4-byte word is not iloc, loop: slice = the last 2
if next 4-byte word is iloc, we offset + 0x2C and this is the offsetChunk_length!
read chunk_length and move on
*/
let offsetChunk_length = metaOffset + 4;
let slice = 0xC;
while (offsetChunk_length < avif.length / 2) {
let word = String.fromCharCode(...avif.slice(offsetChunk_length, offsetChunk_length+4));
if (word != "iloc") {
offsetChunk_length += slice; // next offset to read from
slice = dataView.getUint16(offsetChunk_length - 2); // get new slice length
} else break;
}
offsetChunk_length += 0x2C;
let chunk_length = dataView.getUint32(offsetChunk_length);

/*
dataView.getUint32() reads 4 bytes starting at the specified byte offset of DataView
dataView.getUint16() reads 2 bytes starting at the specified byte offset of DataView
dataView.getUint8() reads 1 bytes starting at the specified byte offset of DataView
https://stackoverflow.com/questions/7555842/questions-about-exif-in-hexadecimal-form
The Exif APP1 Section is actually Exif\0\0 then followed by TIFF Header of 10-bytes length
therefore, we have at 0x15F: 6-bytes Exif\0\0 then 10-bytes Tiff Header

Some Exif color data starts at 0xCA offset, but since we (we = save image custom nodes) add more Exif metadata,
we have our own block starting at 0x15F. I generated many avif and they all start at 0x15F.
We can assume valid avif should come from those custom nodes anyways. Don't you think?
45 78 69 66 00 00 starts at offset = metaOffset + 4 + metaSize + 8: Exif + \0\0 = 6-bytes
E x i f 00 00
Tiff header after is 0xA long (10 bytes) and contains information about byte-order of IFD sections and a pointer to the 0th IFD
Tiff header:
4D 4D 00 2A 00 00 00 08 00 02
-----| |
49 49| | (II for Intel) if the byte-order is little-endian
4D 4D| | (MM for Motorola) for big-endian
|00 2A| magic bytes 0x002A (=42, haha...)
| 08| following 4-byte will tell the offset to the 0th IFD from the start of the TIFF header.
| 02 last 2-byte seem like the number of extra Exif IFD, we have indeed only 2 in this example

IFD Fields are 12-byte subsections of IFD sections
For example, if we are looking for only 2 IFD fields with 0x9286/UserComment/prompt and 0x010e/ImageDescription/workflow:
4D 4D 00 2A 00 00 00 08 00 02 0th IFD, 10-byte long, last 2 bytes give how many IFD fields there are

01 0E 00 02 00 00 2F 15 00 00 00 26 010e/ImageDescription IFD1
92 86 00 02 00 00 0F 7C 00 00 2F 3C 9286/UserComment IFD2
-----| tag ID
|-----| type of the field data. 1 for byte, 2 for ascii, 3 for short (uint16), 4 for long (uint32), etc
|-----------| length in 4 bytes, only for ascii, which we only care about; max is 4GB of data
|-----------| 4-byte field value, no idea what that's for

00 00 00 00 then 4-byte offset from the end of the TIFF header to the start of the first IFD value
Workflow: {"...0.4} 00 W starts at offset 0x18B (don't care) and is length 0x2F15 including 1 last \0
00 00 separator
Prompt: {"... } 00 P starts at offset 0x30A1 (don't care) and is length 0x0F7C including 1 last \0

*/
let txt_chunks = {}
// Loop through the chunks in the AVIF file
// avif clearly are different beasts than webp, there is only one chunck of Exif data at the beginning.
// If we ever come across one that is different, surely it's not been produced by a custom node and surely, the metadata is invalid
// while (offset < (offset + chunk_length)) { // no need to scan the whole avif file
const chunk_type = String.fromCharCode(...avif.slice(offset, offset + 6));
if (chunk_type === "Exif\0\0") {
offset += 6;

// parseExifData must start at the Tiff Header: 0x4949 or 0x4D4D for Big-Endian
let data = parseExifData(avif.slice(offset, offset + chunk_length));
for (var key in data) {
if (data[key]) {
var value = data[key];
let index = value.indexOf(':');
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
}
}
}

// offset += chunk_length;
// }

r(txt_chunks);
};

reader.readAsArrayBuffer(file);
});
}

export function getLatentMetadata(file) {
return new Promise((r) => {
const reader = new FileReader();
Expand Down