how to read exif hexadecimal metadata from avif with javascript featured meme

How To Read Exif Hexadecimal Metadata from AVIF with JavaScript

How To Read Exif Hexadecimal Metadata from AVIF with JavaScript? Here is how, based off some random info gathered from Stackoverflow. Impossible to find any data online, the MIAB and ISO publications are not free. Go figure.

AVIF, HEIC, WebP, JXL, Késako?

Alliance_for_Open_Media_logo
Alliance for Open Media logo

As we look for alternatives to replace the aging JPEG image format, AVIF, HEIC, and JPEG XL have emerged as strong contenders. AVIF, developed by the Alliance for Open Media, offers excellent compression and is expected to become the dominant format for web delivery. HEIC, primarily supported in the Apple ecosystem, provides superior compression for full-color photos. JPEG XL, being developed by JPEG, Google, and Cloudinary, offers a balance between compression and visual fidelity along with advanced features.

Read this excellent in-depth comparison of these new image formats, where WebP is not even mentionned. WebP is pushed by Google, and it was a good idea to replace JPEG. Unfortunately, once you see what AVIF can do, WebP can be forgotten completely. There are many blogs comparing WebP vs AVIF, and even Adobe tried at some point, with this article of very poor quality (and many typos).

In AEM Dynamic Media, PNG image format is considered to be lossless. Hence all PNG images are always delivered at 100 quality. In this comparison we compared the images at quality setting of 90 for JPEG/WebP and 50 for AVIF. But this quality number is subjective to each format.

How do we ensure if the image has same visual quality? Answer is PSNR (Peak Signal to Noise Ratio). PSNR is a good measure for comparing restoration results for the same image. PSNR of JPEG/WebP/AVIF image is calculated with respect to its PNG image.

Here one can observe WebP at quality 90 and AVIF at quality 50 maintains the similar PSNR when compared to JPEG at quality 90:

webp avif have similar psnr when compared to jpeg at quality 90
webp avif have similar psnr when compared to jpeg at quality 90

The chart below shows the same images size’, for Webp quality=90 vs AVIF quality=50:

same images size with jpeg vs webp vs avif
same images size with jpeg vs webp vs avif

AVIF always beats WebP in size, for similar visual quality. Only problem is its CPU consumption: AVIF encoding requires 4x more CPU power then WebP.

Use Case Scenario to Read Exif from AVIF

ComfyUI currently (recently) added WebP support. Formerly it could only read prompts from PNG files. Now that @comfyanonymous fixed WebP support, let’s add AVIF!

Why AVIF? Not gonna enter a debate others already have discussed. It’s just better, open source, smaller, can handle transparency, etc. Based on heic and AV1 codecs already used by H.264, Youtube, Apple etc. The only downside is the CPU power needed to compress.

In our very specific use case scenarion, we are reading IFD Exif tags, that will represent ASCII JSON dumps from the workflow we want to load in ComfyUI. Tags can be any tags, but the best to use for this case are:

  • 0x010e = ImageDescription = workflow
  • 0x010f= Make = prompt

A ComfyUI custom node such as Save Image Extended or WAS Node Suite will always follow the below best practices when saving the prompt/workflow in Exif tags:

from PIL import Image, ExifTags
import numpy as np
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata["prompt"] = prompt

## This method gives good results, as long as you save the prompt/workflow in 2 separate Exif tags
## Otherwise, [ExifTool] will issue Warning: Invalid EXIF text encoding for Make
## exif type is PIL.Image.Exif
exif = img.getexif()

## It seems better to separate the two
# 0x010e: ImageDescription
# 0x010f: Make
# interestingly, workflow will come first in the Exif blocks inside avif
exif[0x010f] = "Prompt: " + json.dumps(metadata['prompt'])     # Make
exif[0x010e] = "Workflow: " + json.dumps(metadata['workflow']) # ImageDescription

# Add more kwargs as needed: pnginfo=metadata, compress_level=self.png_compress_level etc
kwargs["exif"] = exif.tobytes()

img.save(image_path, **kwargs)

 

How To Read Exif From AVIF

AVIF File Signature

Very simple: we read from offest 4 and 8, and look for ftyp attached to avif or heic. HEIC is the licensed, unusable, concurrent codec from Apple:

avif file signature
avif file signature

In Javascript, that could look like this:

export function getAvifMetadata(file) {
  return new Promise((r) => {
    const reader = new FileReader();
    reader.onload = (event) => {
      const avif = new Uint8Array(event.target.result);
      console.log('event.target.result',event.target.result); // this is a decimal dump of the file: 0, 0, 0, 32, 102, == 00 00 00 20 ..
      console.log('avif',avif);
      const dataView = new DataView(avif.buffer);
      console.log('dataView',dataView);

      // 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;
      }

Important Javascript functions to use here:

  • 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

AVIF Metadata Core Chunk Analysis

After the signature, we have some stuff defining the kind of avif it is, thecompatible Brands: [avif,mif1,miaf,MA1B,etc], the codecs etc, and then a 00 00 separator, and then 2-bytes defining the size of the core section:

example1 native avif vs magick 2avif core image length
example1 native avif vs magick 2avif core image length

The examples above are the exact same image generated with exact same workflow. Only difference is, the second one was a png converted to AVIF with imagick + exiftool to re-import the prompt:

 

avif euler 0001
avif euler 0001
avif euler 0001
avif euler 0001
avif euler 0001 2avif
avif euler 0001 2avif

 

So,  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 different compatible brands: avifmiflmiaf (image magick) or avifmiflmiafMA1B (PIL.Image)
  • 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

6-byte Exif IFD section offset position

Following the explanations from above, here is how to calculate the 6-byte Exif\0\0 position:

  • 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!
example1 calculate exif ifd position
example1 calculate exif ifd position

In Javascript, that gives this:

let metaOffset = 0x10
while (metaOffset < avif.length / 2) {
  let word = String.fromCharCode(...avif.slice(metaOffset, metaOffset+4));
  // console.log('word',avif.slice(metaOffset, metaOffset+4));
  // console.log('word',word);
  if (word.slice(0,2) != "\0\0") {
    metaOffset += 4
  } else break;
}
let metaSize = dataView.getUint32(metaOffset);
let offset = metaOffset + 4 + metaSize + 8;
//console.log('metaSize',metaSize);
//console.log('offset',offset);

 

AVIF Core Image Definitions Sections

This picture summarize it all: they are all of various length, chained to each other, with length of the next section at the end of each section:

example1 hex exif chunks analysis 1
example1 hex exif chunks analysis 1

The only value we are looking for in this section is the Exif chunk size:

example1 native avif vs magick 2avif for exif length and length position
example1 native avif vs magick 2avif for exif length and length position

 

Exif Chunk Size Position Method

The Exif chunk size is 4-byte long, defined by iloc, and succeeded by iinf.

  • 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_size and break loop
example1 calculate exif chunk length position
example1 calculate exif chunk length position

In JavaScript, that gives this:

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") {
    console.log('word',word);
    offsetChunk_length += slice; // next offset to read from
    slice = dataView.getUint16(offsetChunk_length - 2); // get new slice length
    console.log('next slice:',slice);
  } else break;
}
offsetChunk_length += 0x2C;
let chunk_length = dataView.getUint32(offsetChunk_length);
console.log('chunk_length',chunk_length);

 

How To read those Exif Metadata

Exif sections are pretty similar among AVIF, JPEG and WEBP, but placed at different positions in the file

The Exif APP1 Section is a 6-byte Exif\0\0 followed by TIFF Header of 10-bytes length. Therefore, after the 6-bytes Exif\0\0 we have 10-byte Tiff Header.

I generated many avif with PIL.Image and the structure is always the same. Only when created with Imagick, do we have slight differences in the meta section.

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 0x010f/Make/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
01 0F 00 02 00 00 0F 7C 00 00 2F 3C   010f/Make             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

If that is not clear, don’t worry. Just open the file with HxD or whatever hexaddecimal tool you like and spend more time on it. All of this is corrobored by exiftool:

`exiftool -struct image.avif`

exiftool output for a classic avif file
exiftool output for a classic avif file

How to Code Exif Chunk Read Hexadecimal in Javascript

An example inside the reader.onload = (event) => {} loop would look like:

let txt_chunks = {}
// Loop through the chunks in the AVIF file
// console.log('chunk_length',chunk_length)
// 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));
  console.log('chunk_type',chunk_type)
  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];
        console.log('data[key]',value)
        let index = value.indexOf(':');
        txt_chunks[value.slice(0, index)] = value.slice(index + 1);
      }
    }
  }

  // offset += chunk_length;
// }

Then the function parseExifData will split the whole Exif chunk into each of its IFD components and return its content.

 

How Similar Exif Metadata look between AVIF JPEG and WEBP

exif blocks hexadecimal comparison avif jpeg webp
exif blocks hexadecimal comparison avif jpeg webp

Only WebP places the Exif IFD data at the bottom of the file, the others place it at the start or close.

Hope that helps anyone on earth… Thanks to Ozgur Ozcitak’s post on Stackoverflow for the help! So unfortunate that we have to reverse-engineer stuff that’s supposed to be open source… Any idea why the specs are not available?

ComfyUI AVIF support PR created, fingers crossed!

 

Leave a Reply

Your email address will not be published. Required fields are marked *