No history yet

TIFF Structure Parsing

Inside the TIFF File

A TIFF file isn't a simple stream of pixels. It's a structured container, like a well-organised filing cabinet. To get anything useful out of it, you first need to read its map. This map starts with an 8-byte header right at the beginning of the file.

This header is your entry point. The first two bytes tell you the file's byte order, or endianness—a crucial piece of information for reading the numbers that follow.

  • II (0x4949) means Little Endian, the standard for Intel processors.
  • MM (0x4D4D) means Big Endian, used by Motorola processors.

The next two bytes are a version number, which is always 42. It's a simple magic number to confirm you're likely dealing with a TIFF. Finally, bytes 4 through 7 contain a 32-bit integer: the offset, in bytes, from the start of the file to the first Image File Directory (IFD).

The Image File Directory

The is the heart of the TIFF structure. It’s a table of contents that lists all the metadata about the image, such as its width, height, compression type, and much more. A TIFF file can contain multiple images, each with its own IFD. The directories are linked together like a chain.

Each IFD begins with a 2-byte count indicating how many entries it contains. This is followed by a series of 12-byte entries, one for each piece of metadata. After all the entries, there's a 4-byte offset pointing to the next IFD in the file. If this offset is zero, it's the last one.

BytesSizeDescription
0-12 bytesTag ID: A number identifying the metadata (e.g., 256 for ImageWidth).
2-32 bytesType: The data type of the value (e.g., 3 for SHORT, 4 for LONG).
4-74 bytesCount: How many values of this type there are.
8-114 bytesValue/Offset: The actual value if it fits in 4 bytes, otherwise an offset to where the value is stored.

Parsing with .NET 8

Manually parsing binary files used to be cumbersome. Modern .NET makes it much safer and more efficient with types like Span<byte>. A span gives you a memory-safe view into an array of bytes without making any copies. This zero-allocation approach is ideal for high-performance file processing.

Combined with the BinaryPrimitives class, we can read multi-byte numbers directly from the span while respecting the file's specified byte order. There's no need for manual byte-swapping or unsafe pointer manipulation.

// Assume 'fileBytes' is a byte[] containing the TIFF file.
// And 'isLittleEndian' is a bool set from the header.

Span<byte> fileSpan = fileBytes;

// Read the offset to the first IFD from the header (bytes 4-7)
int ifdOffset = isLittleEndian
    ? BinaryPrimitives.ReadInt32LittleEndian(fileSpan.Slice(4, 4))
    : BinaryPrimitives.ReadInt32BigEndian(fileSpan.Slice(4, 4));

// Jump to the IFD and read the number of entries
Span<byte> ifdSpan = fileSpan.Slice(ifdOffset);
short entryCount = isLittleEndian
    ? BinaryPrimitives.ReadInt16LittleEndian(ifdSpan.Slice(0, 2))
    : BinaryPrimitives.ReadInt16BigEndian(ifdSpan.Slice(0, 2));

// Loop through each 12-byte tag entry
for (int i = 0; i < entryCount; i++)
{
    // Each entry is 12 bytes long. Start after the 2-byte entry count.
    int entryStart = 2 + (i * 12);
    Span<byte> entrySpan = ifdSpan.Slice(entryStart, 12);

    // Read the Tag ID (bytes 0-1 of the entry)
    short tagId = isLittleEndian
        ? BinaryPrimitives.ReadInt16LittleEndian(entrySpan.Slice(0, 2))
        : BinaryPrimitives.ReadInt16BigEndian(entrySpan.Slice(0, 2));

    // Example: Check for the ImageWidth tag (ID 256)
    if (tagId == 256)
    {
        // Read the value from bytes 8-11
        // Type and count checks omitted for brevity
        int imageWidth = isLittleEndian
            ? BinaryPrimitives.ReadInt32LittleEndian(entrySpan.Slice(8, 4))
            : BinaryPrimitives.ReadInt32BigEndian(entrySpan.Slice(8, 4));
        Console.WriteLine($"Image Width: {imageWidth}");
    }
}

This code snippet demonstrates the core logic. You first read the IFD offset from the header. Then, you jump to that location, read the number of entries, and loop through them. For each 12-byte entry, you can extract the tag ID, type, count, and value, choosing the correct BinaryPrimitives method based on the endianness you detected at the very beginning.

By following the IFD offsets and parsing the tag entries, you can map out the entire structure of a TIFF file and extract any information you need, all without relying on large, complex image processing libraries.

Time to check your understanding of the TIFF structure.

Quiz Questions 1/6

What is the primary purpose of the first two bytes in a TIFF file header?

Quiz Questions 2/6

You are examining a TIFF file and find that its header begins with the ASCII characters 'MM'. This indicates the file uses Big Endian byte order.

With this foundation, you can now read the essential metadata from any TIFF file. Next, we'll look at how to locate and handle the actual image data strips.