Accessing Fujifilm film simulations in JavaScript

Started by sambecker, October 24, 2023, 12:47:44 PM

Previous topic - Next topic

sambecker

Super naive question:

I'm trying to find the film mode and saturation in a MakerNote binary, and am having trouble accessing them at the locations used by ExifTool (0x1401 and 0x1003, respectively). For some reason, film mode shows up reliably at 0x257 (attached) and saturation is nowhere to be found. In fact, according to my byte array, both addresses are out of range (my binary ends at 0x503). Any idea what I'm doing wrong?

I'm leaning on a framework to get the MakerNote binary, but I'm beginning to think I should just parse the entire jpg file. Do MakerNote's start at a reliable byte address or are they dependent on the content that comes before?

Apologies if this is the wrong forum to ask. Thank you for building this miraculous tool!

Phil Harvey

Try looking at the file using the ExifTool -htmlDump output instead of a generic hex dump.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

sambecker

This is super useful!

Am I right in thinking that Film Mode is 0x1401 in the MakerNote but (approximately) 0xb2a in the file itself? And saturation is 0x1003 in the MakerNote but (approximately) 0x932 in the file itself?

Could that be right, if the file address is a lower number than the MakerNote address? And if so, will those file-based addresses be stable?

StarGeek

Quote from: sambecker on October 24, 2023, 12:47:44 PMI'm trying to find the film mode and saturation in a MakerNote binary, and am having trouble accessing them at the locations used by ExifTool (0x1401 and 0x1003, respectively). For some reason, film mode shows up reliably at 0x257 (attached) and saturation is nowhere to be found. In fact, according to my byte array, both addresses are out of range (my binary ends at 0x503). Any idea what I'm doing wrong?

You're confusing tag IDs with byte locations. You have to parse through the EXIF data until you find the tag with ID of 0x927c.  That is the start of the MakerNotes, though I could be wrong on the exact details here.

Then you have to parse the MakerNotes IDs until you find the right tag ID.

This is further complicated by the ExifByteOrder.

Here's an example using the -htmlDump option.  Notice how the tag ID at the beginning of that line is 01 14?  That's because this file has a Little-endian byte order


"It didn't work" isn't helpful. What was the exact command used and the output.
Read FAQ #3 and use that cmd
Please use the Code button for exiftool output

Please include your OS/Exiftool version/filetype

sambecker

OKā€”that makes a ton of sense. In the attached example, it seems like 1 is the Tag ID in reverse byte order, and 3 is the unsigned int representing Film Mode in reverse byte order. If that's true, what do 2 and 4 represent?

TAG_ID_ANNOTATED.png

Phil Harvey

The tooltip gives you the meanings of all these bytes.  Read the TIFF specification if you want to know what the exact format is.

Note that you must properly process the hierarchical TIFF structure to find the information you need.  You can't just assume a fixed offset in the file.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

sambecker

Thank you everyone! This was exactly what I needed. Here's how I ended up handling in JS:

const parseFujifilmMakerNote = (
  bytes: Buffer,
  valueForTag: (tag: number, value: number) => void
) => {
  for (
    let i = BYTE_INDEX_FIRST_TAG;
    i + BYTES_PER_TAG < bytes.length;
    i += BYTES_PER_TAG
  ) {
    const tag = bytes.readUInt16LE(i);
    const value = bytes.readUInt16LE(i + BYTE_OFFSET_FOR_INT_VALUE);
    valueForTag(tag, value);
  }
};

Magic values (only configured for uint16 values):
const BYTE_INDEX_FIRST_TAG = 14;
const BYTES_PER_TAG = 12;
const BYTE_OFFSET_FOR_INT_VALUE = 8;

greybeard

#7
Your code risks false positives as you are searching beyond the end of the tag area (I'm assuming your framework gives the complete makernotes).

If you look at you png file in the first post:
- the first 8 bytes are always "FUJIFILM"
- bytes offset 8-9 = 0x0c00 (decimal 12 in little endian format - offset to maker notes tag count)
- bytes offset 12-13 = 0x4b00 (decimal 75 in little endian format - this is number of 12 byte tags)
- there are then 75 blocks of 12 bytes for each maker notes tag

You should avoid searching past the 75 blocks.

Within each 12 byte tag block:
- bytes offset 0-1 : tag identity in little endian format such as 0x1401 for color film simulation
- bytes offset 2-3 : tag type (just by chance the color film simulation and saturation or non-color film simulation tags are type 3 which is a 16 bit short)
- bytes offset 4-7 : value count (color film simulation and satuation counts are always 1)
- bytes offset 8-11 : contains the value in little endian format or an offset if the value won't fit into 4 bytes - color film simulation and saturation only take up two bytes so you use the first two bytes - in your example 0x0500 for tag 0x1401 in little endian format for Velvia.

If you need to expand your code to other maker notes tags you are going to have to check the type and count fields as your code will probably not work for other tag types.

As you have discovered if you want Film Simulation you first check for the existence of tag 0x1401 and if it exists you decode it as a color film simulation - if tag 0x1401 does not exist then you take tag 0x1003 and decode it as a non-color film simulation.

And all this assumes you have already found maker notes (and that the tags are stored in little endian format). Maker notes are likely in a different location for every image file. Things will also differ depending on whether you are reading a SOOC jpeg, raf or hif file - or a file which has been processed by some sort of editing or conversion software.

sambecker

This is so great @greybeard thanks for the additional context, especially the bit about the composition of those 12-byte blocks. Feels critical for parsing anything more complex than unsigned ints.

In the code sample I shared, I'm only crawling through the MakerNote block (starting where the tags begin) so that's why I parse every whole unit of 12 bytes until I reach the end of the buffer. To your point, I could step through the 75 blocks spec'd at the top of the file, but I feel like I would still do a range check (i + BYTES_PER_TAG < bytes.length) anyway for safety? What are the risks with that approach?

greybeard

The range check wouldn't do any harm - the problem is not restricting the search to the tag area.

There is additional space at the end of the maker notes which stores values that won't fit into the 4 bytes at the end of each 12 byte tag.

Your code is risking reading this area as though it contained 12 byte tags.

Look at the tags which show up as purple in Phil's htmlDump output - you can see an offset in the 12 byte tag with the actual value stored at the end of the maker notes area.

You can also test this theory by calculating the space you should be reading i.e. 14 + (tag count * 12) and comparing that to the size of the maker notes buffer being supplied by your framework.

sambecker

Once again @greybeard makes total sense. Here's a tighter version of what I'm working with now in case it's helpful to anyone:

const parseFujifilmMakerNote = (
  bytes: Buffer,
  valueForTagUInt: (tagId: number, value: number) => void
) => {
  const tagCount = bytes.readUint16LE(BYTE_INDEX_TAG_COUNT);
  for (let i = 0; i < tagCount; i++) {
    const index = BYTE_INDEX_FIRST_TAG + i * BYTES_PER_TAG;
    if (index + BYTES_PER_TAG < bytes.length) {
      const tagId = bytes.readUInt16LE(index);
      const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
      switch (tagType) {
      // UInt16
      case 3:
        valueForTagUInt(
          tagId,
          bytes.readUInt16LE(index + BYTE_OFFSET_TAG_VALUE),
        );
        break;
      // UInt32
      case 4:
        valueForTagUInt(
          tagId,
          bytes.readUInt32LE(index + BYTE_OFFSET_TAG_VALUE),
        );
        break;
      }
    }
  }
};

And the constants:

const BYTE_INDEX_TAG_COUNT = 12;
const BYTE_INDEX_FIRST_TAG = 14;
const BYTES_PER_TAG = 12;
const BYTE_OFFSET_TAG_TYPE = 2;
const BYTE_OFFSET_TAG_VALUE = 8;

hal9e3

Sorry for necroing this thread, I might not be understanding the logic behind the code but how could I maybe extract WhiteBalancefine? It's a int32s[2]

0x100a   WhiteBalanceFineTune   int32s[2]   (newer cameras should divide these values by 20)

The code below parses int16u fine though :)

0x1002   WhiteBalance   int16u   
0x0 = Auto
0x1 = Auto (white priority)
0x2 = Auto (ambiance priority)
etc...

The code for parsing the MakerNote is this so far:
const BYTE_INDEX_TAG_COUNT = 12;
const BYTE_INDEX_FIRST_TAG = 14;
const BYTES_PER_TAG = 12;
const BYTE_OFFSET_TAG_TYPE = 2;
const BYTE_OFFSET_TAG_VALUE = 8;

const parseFujifilmMakerNote = (
  bytes: Buffer,
  valueForTagUInt: (tagId: number, value: number) => void,
) => {
  const tagCount = bytes.readUint16LE(BYTE_INDEX_TAG_COUNT);
  for (let i = 0; i < tagCount; i++) {
    const index = BYTE_INDEX_FIRST_TAG + i * BYTES_PER_TAG;
    if (index + BYTES_PER_TAG < bytes.length) {
      const tagId = bytes.readUInt16LE(index);
      const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
      switch (tagType) {
      // UInt16
      case 3:
        valueForTagUInt(
          tagId,
          bytes.readUInt16LE(index + BYTE_OFFSET_TAG_VALUE),
        );
        break;
      // UInt32
      case 4:
        valueForTagUInt(
          tagId,
          bytes.readUInt32LE(index + BYTE_OFFSET_TAG_VALUE),
        );
        break;
      }
    }
  }
};

greybeard

That sample code only works for tag types 3 and 4 - and only where the tag value count is 1.

You are going to have to extend it in a number of ways:
- fetch the value count from the 12 byte block (4 byte integer at offset 4 within the block)
- add extra tag types to the switch cases (for example tag 0x100e is tag type 9)
- calculate space needed for the tag values (for example tag 0x100e requires 8 bytes - count = 2 int32s = 4 bytes)

Within the 12 byte tag block there is only space for the actual tag values if they don't need more than 4 bytes.

If space needed is greater than 4 bytes then the value is actually an offset rather than the value itself.

Therefore for White Balance Fine Tune you extract the value and use it as an offset within the maker notes and  extract the two 4 byte WB Fine Tune components.



sambecker

#13
Expanded my implementation to support multiple values across more data types!

Posting here in case anyone's curious (will share a GitHub permalink once this work's merged).

export const parseFujifilmMakerNote = (
  bytes: Buffer,
  sendTagNumbers: (tagId: number, numbers: number[]) => void,
) => {
  const tagCount = bytes.readUint16LE(BYTE_OFFSET_TAG_COUNT);

  for (let i = 0; i < tagCount; i++) {
    const index = BYTE_OFFSET_FIRST_TAG + i * BYTES_PER_TAG;

    if (index + BYTES_PER_TAG < bytes.length) {
      const tagId = bytes.readUInt16LE(index);
      const tagType = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_TYPE);
      const tagValueSize = bytes.readUInt16LE(index + BYTE_OFFSET_TAG_SIZE);

      const sendNumbersForDataType = (
        calculateNumberForOffset: (offset: number) => number,
        sizeInBytes: number,
      ) => {
        let values: number[] = [];
        if (tagValueSize * sizeInBytes <= BYTES_PER_TAG_VALUE) {
          // Retrieve values if they fit in tag block
          values = Array.from({ length: tagValueSize }, (_, i) =>
            calculateNumberForOffset(
              index + BYTE_OFFSET_TAG_VALUE + i * sizeInBytes,
            ),
          );
        } else {
          // Retrieve outside values if they don't fit in tag block
          const offset = bytes.readUint16LE(index + BYTE_OFFSET_TAG_VALUE);
          values = [];
          for (let i = 0; i < tagValueSize; i++) {
            values.push(calculateNumberForOffset(offset + i * sizeInBytes));
          }
        }
        sendTagNumbers(tagId, values);
      };

      switch (tagType) {
      // Int8 (UInt8 read as Int8 according to spec)
      case 1:
        sendNumbersForDataType(offset => bytes.readInt8(offset), 1);
        break;
      // UInt16
      case 3:
        sendNumbersForDataType(offset => bytes.readUInt16LE(offset), 2);
        break;
      // UInt32
      case 4:
        sendNumbersForDataType(offset => bytes.readUInt32LE(offset), 4);
        break;
      // Int32
      case 9:
        sendNumbersForDataType(offset => bytes.readInt32LE(offset), 4);
        break;
      }
    }
  }
};