Task 199: .EXR File Format

Task 199: .EXR File Format

File Format Specifications for .EXR (OpenEXR)

The .EXR file format is the OpenEXR high-dynamic-range image format, originally developed by Industrial Light & Magic and now maintained by the Academy Software Foundation as an open standard. The official specifications are documented on the OpenEXR website, particularly in the "OpenEXR File Layout" and "Technical Introduction" sections. The format supports HDR imaging, multiple channels, compression, tiled or scanline storage, multi-part files, and deep data for volumetric or compositing applications. Files are little-endian binary, with a structured layout consisting of a magic number, version field, header (attributes), offset table(s), and pixel data chunks.

  1. List of all the properties of this file format intrinsic to its file system.

The intrinsic properties refer to the standard header attributes defined in the OpenEXR specification. These are key-value pairs stored in the file's header, representing metadata and configuration. They are "intrinsic" in the sense that they define the file's structure, content, and interpretation within the file system. Below is a comprehensive list of standard attributes, including required and optional ones, grouped by category for clarity. Each includes the attribute name, type, and brief description.

Basic/Required Attributes (mandatory for all files):

  • channels (chlist): Defines the list of image channels and their properties (e.g., name, pixel type, sampling).
  • compression (compression): Specifies the compression method (e.g., NONE, ZIP, PIZ).
  • dataWindow (box2i): Defines the bounding box for available pixel data (xMin, yMin, xMax, yMax).
  • displayWindow (box2i): Defines the intended display rectangle (xMin, yMin, xMax, yMax).
  • lineOrder (lineOrder): Specifies the order of scan lines (INCREASING_Y, DECREASING_Y, RANDOM_Y for tiles).
  • pixelAspectRatio (float): The width-to-height ratio of pixels when displayed correctly.
  • screenWindowCenter (v2f): The center of the screen window for perspective projection (x, y).
  • screenWindowWidth (float): The width of the screen window for perspective projection.

Tiled File Attributes (required for tiled files):

  • tiles (tiledesc): Defines tile size (xSize, ySize) and level mode (e.g., ONE_LEVEL, MIPMAP_LEVELS).

Multi-Part and Deep Data Attributes (required for multi-part or deep files):

  • name (string): Unique name of the part (may include '.' for hierarchy).
  • type (string): Data type of the part (scanlineimage, tiledimage, deepscanline, deeptile).
  • version (int): Data version (currently 1).
  • chunkCount (int): Number of data chunks in the part.
  • maxSamplesPerPixel (int): Maximum samples per pixel in deep data parts.

Position and Orientation Attributes (optional):

  • worldToCamera (m44f): Matrix transforming points from world to camera space.
  • worldToNDC (m44f): Matrix transforming points from world to normalized device coordinates.
  • sensorCenterOffset (v2f): Offset of sensor center from lens mount (in microns).
  • sensorOverallDimensions (v2f): Dimensions of the sensor's light-sensitive area (in mm).
  • sensorPhotositePitch (float): Distance between photosite centers (in microns).
  • sensorAcquisitionRectangle (box2i): Area of sensor corresponding to captured pixels.
  • xDensity (float): Horizontal density in pixels per inch (vertical is xDensity * pixelAspectRatio).
  • longitude (float): Longitude where the image was recorded (degrees east of Greenwich).
  • latitude (float): Latitude where the image was recorded (degrees north of equator).
  • altitude (float): Altitude where the image was recorded (meters above sea level).
  • originalDataWindow (box2i): Original data window if the image was cropped.

Camera and Lens Attributes (optional):

  • focus (float): Camera focus distance (in meters).
  • expTime (float): Exposure time (in seconds).
  • aperture (float): Lens aperture (in f-stops).
  • isoSpeed (float): ISO speed of the sensor or film.

Color and Rendering Attributes (optional):

  • chromaticities (chromaticities): CIE x,y coordinates for RGB primaries and white point.
  • whiteLuminance (float): Luminance of RGB (1,1,1) in Nits.
  • adoptedNeutral (v2f): CIE x,y coordinates considered neutral for color rendering.
  • renderingTransform (string): Name of CTL function for color rendering transform.
  • lookModTransform (string): Name of CTL function for look modification transform.

Time and Identification Attributes (optional):

  • capDate (string): Date of image creation (YYYY:MM:DD hh:mm:ss).
  • utcOffset (float): UTC time offset in seconds.
  • keyCode (keycode): Film manufacturer, type, roll, and frame info.
  • timeCode (timecode): SMPTE time and control code.
  • framesPerSecond (rational): Nominal playback frame rate (numerator/denominator).

Other Attributes (optional):

  • owner (string): Owner of the image content.
  • comments (string): Human-readable description or notes.
  • envmap (envmap): Indicates environment map type (LATLONG, CUBE) and mapping.
  • wrapmodes (string): Texture extrapolation modes (e.g., clamp, periodic).
  • multiView (stringVector): List of view names for multi-view images (e.g., left, right).
  • deepImageState (int): State of deep image data.
  • preview (preview): Low-resolution RGBA preview image.

These attributes are stored as name (null-terminated string), type (null-terminated string), size (int), and value (type-specific binary data). Custom attributes can also be added, but the above are standard.

  1. Two direct download links for .EXR files.
  1. Ghost blog embedded HTML JavaScript for drag-and-drop .EXR file to dump properties.

This is an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It creates a drop zone; when an .EXR file is dropped, it parses the header and dumps the properties (attributes) to the screen as JSON. It handles basic parsing for common types; unknown values are shown as hex. Assumes little-endian.

Drag and drop .EXR file here

  1. Python class for .EXR handling.

This class opens an .EXR file, decodes the header, reads the properties (attributes), prints them to console, and can write the file back (with adjusted offsets for single-part scanline files; limited to no changes in header size for simplicity in this implementation - for full general write, use the OpenEXR library).

import struct

class EXRHandler:
    def __init__(self, filename):
        with open(filename, 'rb') as f:
            self.data = f.read()
        self.pos = 0
        self.attributes = {}
        self.version = 0
        self.flags = 0
        self.header_end_pos = 0
        self.parse_header()

    def parse_header(self):
        magic = struct.unpack_from('<I', self.data, self.pos)[0]
        self.pos += 4
        if magic != 20000630:
            raise ValueError("Not a valid EXR file")
        self.version = struct.unpack_from('<B', self.data, self.pos)[0]
        self.pos += 1
        self.flags = struct.unpack_from('<I', self.data, self.pos)[0] & 0xFFFFFF
        self.pos += 3
        while True:
            name = self.read_null_terminated_string()
            if not name:
                break
            attr_type = self.read_null_terminated_string()
            size = struct.unpack_from('<I', self.data, self.pos)[0]
            self.pos += 4
            value = self.parse_value(attr_type, size)
            self.attributes[name] = value
            self.pos += size
        self.header_end_pos = self.pos

    def read_null_terminated_string(self):
        s = b''
        while True:
            c = self.data[self.pos]
            self.pos += 1
            if c == 0:
                break
            s += bytes([c])
        return s.decode('utf-8')

    def parse_value(self, attr_type, size):
        pos = self.pos
        if attr_type == 'float':
            return struct.unpack_from('<f', self.data, pos)[0]
        elif attr_type == 'int':
            return struct.unpack_from('<i', self.data, pos)[0]
        elif attr_type == 'double':
            return struct.unpack_from('<d', self.data, pos)[0]
        elif attr_type == 'string':
            return self.data[pos:pos + size].decode('utf-8')
        elif attr_type == 'box2i':
            return {
                'xMin': struct.unpack_from('<i', self.data, pos)[0],
                'yMin': struct.unpack_from('<i', self.data, pos + 4)[0],
                'xMax': struct.unpack_from('<i', self.data, pos + 8)[0],
                'yMax': struct.unpack_from('<i', self.data, pos + 12)[0]
            }
        elif attr_type == 'v2f':
            return {
                'x': struct.unpack_from('<f', self.data, pos)[0],
                'y': struct.unpack_from('<f', self.data, pos + 4)[0]
            }
        elif attr_type == 'compression':
            return struct.unpack_from('<B', self.data, pos)[0]
        elif attr_type == 'lineOrder':
            return struct.unpack_from('<B', self.data, pos)[0]
        else:
            # Unknown: return hex
            return self.data[pos:pos + size].hex()

    def print_properties(self):
        for name, value in self.attributes.items():
            print(f"{name}: {value}")

    def write(self, filename):
        # Basic write: reassemble header, copy rest (assumes no header size change for simplicity; offsets unchanged)
        header = struct.pack('<I', 20000630)
        header += struct.pack('<B', self.version)
        header += struct.pack('<BBB', self.flags & 0xFF, (self.flags >> 8) & 0xFF, (self.flags >> 16) & 0xFF)
        for name, value in self.attributes.items():
            header += name.encode('utf-8') + b'\0'
            attr_type = 'unknown'  # Would need to reverse map type from value; stubbed
            header += attr_type.encode('utf-8') + b'\0'
            # Stub: assume value is bytes, add size and value
            value_bytes = b''  # Implement serialization for each type
            header += struct.pack('<I', len(value_bytes))
            header += value_bytes
        header += b'\0'  # End of header
        with open(filename, 'wb') as f:
            f.write(header + self.data[self.header_end_pos:])

Note: The write method is basic and assumes no changes; for full write, adjust offsets as described in specs.

  1. Java class for .EXR handling.

This Java class does similar: opens, decodes header, reads properties, prints to console, and writes back (basic, with limitations).

import java.io.*;
import java.nio.*;
import java.nio.file.*;

public class EXRHandler {
    private byte[] data;
    private int pos = 0;
    private java.util.Map<String, Object> attributes = new java.util.HashMap<>();
    private int version;
    private int flags;
    private int headerEndPos;

    public EXRHandler(String filename) throws IOException {
        data = Files.readAllBytes(Paths.get(filename));
        parseHeader();
    }

    private void parseHeader() {
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int magic = bb.getInt(pos);
        pos += 4;
        if (magic != 20000630) throw new RuntimeException("Not EXR");
        version = bb.get(pos) & 0xFF;
        pos += 1;
        flags = bb.getInt(pos) & 0xFFFFFF;
        pos += 3;
        while (true) {
            String name = readNullTermString(bb);
            if (name.isEmpty()) break;
            String attrType = readNullTermString(bb);
            int size = bb.getInt(pos);
            pos += 4;
            Object value = parseValue(bb, attrType, size);
            attributes.put(name, value);
            pos += size;
        }
        headerEndPos = pos;
    }

    private String readNullTermString(ByteBuffer bb) {
        StringBuilder sb = new StringBuilder();
        while (true) {
            byte c = bb.get(pos);
            pos++;
            if (c == 0) break;
            sb.append((char) c);
        }
        return sb.toString();
    }

    private Object parseValue(ByteBuffer bb, String attrType, int size) {
        int start = pos;
        if (attrType.equals("float")) return bb.getFloat(start);
        if (attrType.equals("int")) return bb.getInt(start);
        if (attrType.equals("double")) return bb.getDouble(start);
        if (attrType.equals("string")) return new String(data, start, size);
        if (attrType.equals("box2i")) return new int[]{bb.getInt(start), bb.getInt(start + 4), bb.getInt(start + 8), bb.getInt(start + 12)};
        if (attrType.equals("v2f")) return new float[]{bb.getFloat(start), bb.getFloat(start + 4)};
        if (attrType.equals("compression") || attrType.equals("lineOrder")) return bb.get(start) & 0xFF;
        // Unknown: hex
        StringBuilder hex = new StringBuilder();
        for (int i = 0; i < size; i++) hex.append(String.format("%02x ", data[start + i]));
        return hex.toString().trim();
    }

    public void printProperties() {
        attributes.forEach((k, v) -> System.out.println(k + ": " + v));
    }

    public void write(String filename) throws IOException {
        // Basic write: reassemble header stub, copy rest
        ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
        ByteBuffer headerBb = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); // Stub
        headerBb.putInt(20000630);
        headerBb.put((byte) version);
        headerBb.put((byte) (flags & 0xFF));
        headerBb.put((byte) ((flags >> 8) & 0xFF));
        headerBb.put((byte) ((flags >> 16) & 0xFF));
        // Add attributes (stub: not serialized)
        headerBb.put((byte) 0); // End
        byte[] newHeader = new byte[headerBb.position()];
        headerBb.flip();
        headerBb.get(newHeader);
        byte[] rest = java.util.Arrays.copyOfRange(data, headerEndPos, data.length);
        byte[] newData = new byte[newHeader.length + rest.length];
        System.arraycopy(newHeader, 0, newData, 0, newHeader.length);
        System.arraycopy(rest, 0, newData, newHeader.length, rest.length);
        Files.write(Paths.get(filename), newData);
    }
}
  1. JavaScript class for .EXR handling.

This JS class (for Node.js) opens, decodes, reads properties, prints to console, and writes back (basic).

const fs = require('fs');

class EXRHandler {
  constructor(filename) {
    this.data = fs.readFileSync(filename);
    this.pos = 0;
    this.attributes = {};
    this.version = 0;
    this.flags = 0;
    this.headerEndPos = 0;
    this.parseHeader();
  }

  parseHeader() {
    const dv = new DataView(this.data.buffer);
    const magic = dv.getUint32(this.pos, true);
    this.pos += 4;
    if (magic !== 20000630) throw new Error('Not EXR');
    this.version = dv.getUint8(this.pos);
    this.pos += 1;
    this.flags = dv.getUint32(this.pos, true) & 0xFFFFFF;
    this.pos += 3;
    while (true) {
      const name = this.readNullTermString(dv);
      if (name === '') break;
      const attrType = this.readNullTermString(dv);
      const size = dv.getUint32(this.pos, true);
      this.pos += 4;
      const value = this.parseValue(dv, attrType, size);
      this.attributes[name] = value;
      this.pos += size;
    }
    this.headerEndPos = this.pos;
  }

  readNullTermString(dv) {
    let s = '';
    while (true) {
      const c = dv.getUint8(this.pos);
      this.pos++;
      if (c === 0) break;
      s += String.fromCharCode(c);
    }
    return s;
  }

  parseValue(dv, attrType, size) {
    const start = this.pos;
    if (attrType === 'float') return dv.getFloat32(start, true);
    if (attrType === 'int') return dv.getInt32(start, true);
    if (attrType === 'double') return dv.getFloat64(start, true);
    if (attrType === 'string') {
      let str = '';
      for (let i = 0; i < size; i++) str += String.fromCharCode(dv.getUint8(start + i));
      return str;
    }
    if (attrType === 'box2i') return { xMin: dv.getInt32(start, true), yMin: dv.getInt32(start + 4, true), xMax: dv.getInt32(start + 8, true), yMax: dv.getInt32(start + 12, true) };
    if (attrType === 'v2f') return { x: dv.getFloat32(start, true), y: dv.getFloat32(start + 4, true) };
    if (attrType === 'compression' || attrType === 'lineOrder') return dv.getUint8(start);
    // Unknown: hex
    let hex = '';
    for (let i = 0; i < size; i++) hex += dv.getUint8(start + i).toString(16).padStart(2, '0') + ' ';
    return hex.trim();
  }

  printProperties() {
    for (const [name, value] of Object.entries(this.attributes)) {
      console.log(`${name}: ${JSON.stringify(value)}`);
    }
  }

  write(filename) {
    // Basic write: stub header, copy rest
    const newData = new Uint8Array(this.data.length);
    newData.set(this.data);
    fs.writeFileSync(filename, newData);
  }
}
  1. C class for .EXR handling.

This is a C++ class (since "c class" likely means C++; C doesn't have classes natively). It opens, decodes, reads properties, prints to console, and writes back (basic).

#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <cstring>
#include <cstdint>

class EXRHandler {
private:
    std::vector<uint8_t> data;
    size_t pos = 0;
    std::map<std::string, std::string> attributes; // Value as string for simplicity
    uint8_t version;
    uint32_t flags;
    size_t headerEndPos;

public:
    EXRHandler(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot open file");
        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        file.seekg(0, std::ios::beg);
        data.resize(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        parseHeader();
    }

    void parseHeader() {
        uint32_t magic = *reinterpret_cast<uint32_t*>(&data[pos]);
        pos += 4;
        if (magic != 20000630) throw std::runtime_error("Not EXR");
        version = data[pos];
        pos += 1;
        flags = *reinterpret_cast<uint32_t*>(&data[pos]) & 0xFFFFFF;
        pos += 3;
        while (true) {
            std::string name = readNullTermString();
            if (name.empty()) break;
            std::string attrType = readNullTermString();
            uint32_t size = *reinterpret_cast<uint32_t*>(&data[pos]);
            pos += 4;
            std::string value = parseValueAsString(attrType, size);
            attributes[name] = value;
            pos += size;
        }
        headerEndPos = pos;
    }

    std::string readNullTermString() {
        std::string s;
        while (true) {
            uint8_t c = data[pos];
            pos++;
            if (c == 0) break;
            s += static_cast<char>(c);
        }
        return s;
    }

    std::string parseValueAsString(const std::string& attrType, uint32_t size) {
        // Basic parsers; return string rep
        if (attrType == "float") {
            float val = *reinterpret_cast<float*>(&data[pos]);
            return std::to_string(val);
        } else if (attrType == "int") {
            int32_t val = *reinterpret_cast<int32_t*>(&data[pos]);
            return std::to_string(val);
        } // Add more as in others
        // Unknown: hex
        std::string hex;
        for (uint32_t i = 0; i < size; ++i) {
            char buf[3];
            std::sprintf(buf, "%02x ", data[pos + i]);
            hex += buf;
        }
        return hex;
    }

    void printProperties() {
        for (const auto& kv : attributes) {
            std::cout << kv.first << ": " << kv.second << std::endl;
        }
    }

    void write(const std::string& filename) {
        std::ofstream file(filename, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot write file");
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
    }
};