Task 063: .BLEND File Format

Task 063: .BLEND File Format

.BLEND File Format Specifications

The .BLEND file format is Blender's native binary format for storing 3D scenes, models, animations, settings, and other data. It is not intended as an interchange format but rather a structured memory dump with metadata for compatibility across Blender versions. Based on available specifications, it consists of a 12-byte header followed by a series of file blocks in a TLV-like structure, ending with a "DNA1" block that describes the data schema (Structure DNA) used in the file. The format supports both 32-bit and 64-bit pointers, little- or big-endian byte order, and can optionally be gzip-compressed (in which case the file starts with the gzip magic bytes 0x1F 0x8B instead of "BLENDER").

List of Properties Intrinsic to the File Format:

  • Magic String: A 7-byte ASCII identifier "BLENDER" to confirm the file type (for uncompressed files).
  • Pointer Size Indicator: A single byte, '_' for 32-bit (4 bytes) or '-' for 64-bit (8 bytes).
  • Endianness Indicator: A single byte, 'v' for little-endian or 'V' for big-endian.
  • Version Number: A 3-byte ASCII string representing the Blender version (e.g., "300" for Blender 3.0.0).
    These are the core header properties. Additional intrinsic aspects include optional gzip compression (detected via first two bytes 0x1F 0x8B), followed by file blocks with codes (4-byte ASCII), body length (u4), old memory address (pointer-sized), SDNA index (u4), count (u4), and body data. The "DNA1" block at the end provides a schema with names, types, lengths, and structs.

Two Direct Download Links for .BLEND Files:

HTML JavaScript for Drag-and-Drop .BLEND File Dump:
Here's embeddable HTML with JavaScript for a simple web page (suitable for a Ghost blog post or any HTML context). It allows dragging and dropping a .BLEND file, checks for compression, parses the header properties, and dumps them to the screen. It assumes an uncompressed file for full parsing but detects gzip.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>.BLEND File Properties Dumper</title>
    <style>
        body { font-family: Arial, sans-serif; }
        #drop-zone { border: 2px dashed #ccc; padding: 20px; text-align: center; }
        #output { margin-top: 20px; white-space: pre-wrap; }
    </style>
</head>
<body>
    <h1>Drag and Drop .BLEND File</h1>
    <div id="drop-zone">Drop a .BLEND file here</div>
    <div id="output"></div>
    <script>
        const dropZone = document.getElementById('drop-zone');
        const output = document.getElementById('output');

        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#333';
        });

        dropZone.addEventListener('dragleave', () => {
            dropZone.style.borderColor = '#ccc';
        });

        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#ccc';
            const file = e.dataTransfer.files[0];
            if (file && file.name.endsWith('.blend')) {
                const reader = new FileReader();
                reader.onload = (event) => {
                    const arrayBuffer = event.target.result;
                    const uint8Array = new Uint8Array(arrayBuffer);
                    let props = '';

                    // Check for gzip compression
                    if (uint8Array[0] === 0x1F && uint8Array[1] === 0x8B) {
                        props += 'Compression: Gzip compressed\n';
                        // Decompression not implemented in pure JS here; assume uncompressed for further parsing.
                    } else {
                        props += 'Compression: Uncompressed\n';
                    }

                    // Parse header (assuming uncompressed or after decompression)
                    const magic = String.fromCharCode(...uint8Array.slice(0, 7));
                    props += `Magic String: ${magic}\n`;

                    const ptrSizeId = String.fromCharCode(uint8Array[7]);
                    const ptrSize = ptrSizeId === '_' ? 4 : 8;
                    props += `Pointer Size Indicator: ${ptrSizeId} (${ptrSize} bytes)\n`;

                    const endianId = String.fromCharCode(uint8Array[8]);
                    const endian = endianId === 'v' ? 'little-endian' : 'big-endian';
                    props += `Endianness Indicator: ${endianId} (${endian})\n`;

                    const version = String.fromCharCode(...uint8Array.slice(9, 12));
                    props += `Version Number: ${version}\n`;

                    output.textContent = props;
                };
                reader.readAsArrayBuffer(file);
            } else {
                output.textContent = 'Please drop a valid .BLEND file.';
            }
        });
    </script>
</body>
</html>

Python Class for .BLEND File Handling:
This class opens a .BLEND file, decodes the header properties, prints them to console, and includes a write method to create a minimal new .BLEND file with custom header (note: full writing of complex .BLEND data is beyond scope; this writes a basic header-only file for demonstration).

import struct
import gzip
import os

class BlendFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.magic = None
        self.ptr_size_id = None
        self.ptr_size = None
        self.endian_id = None
        self.endian = None
        self.version = None
        self.is_compressed = False
        self._read_and_decode()

    def _read_and_decode(self):
        with open(self.filepath, 'rb') as f:
            data = f.read(12)  # Read header size
            if data[:2] == b'\x1f\x8b':
                self.is_compressed = True
                with gzip.open(self.filepath, 'rb') as gf:
                    data = gf.read(12)
            self.magic = data[:7].decode('ascii')
            self.ptr_size_id = chr(data[7])
            self.ptr_size = 4 if self.ptr_size_id == '_' else 8
            self.endian_id = chr(data[8])
            self.endian = 'little-endian' if self.endian_id == 'v' else 'big-endian'
            self.version = data[9:12].decode('ascii')

    def print_properties(self):
        print(f"Compression: {'Gzip compressed' if self.is_compressed else 'Uncompressed'}")
        print(f"Magic String: {self.magic}")
        print(f"Pointer Size Indicator: {self.ptr_size_id} ({self.ptr_size} bytes)")
        print(f"Endianness Indicator: {self.endian_id} ({self.endian})")
        print(f"Version Number: {self.version}")

    def write_new_file(self, new_filepath, version='300', ptr_size=8, endian='little', compress=False):
        ptr_size_id = '-' if ptr_size == 8 else '_'
        endian_id = 'v' if endian == 'little' else 'V'
        header = b'BLENDER' + ptr_size_id.encode('ascii') + endian_id.encode('ascii') + version.encode('ascii')
        data = header  # Minimal; add blocks in real use
        if compress:
            with gzip.open(new_filepath, 'wb') as f:
                f.write(data)
        else:
            with open(new_filepath, 'wb') as f:
                f.write(data)

# Example usage:
# handler = BlendFileHandler('example.blend')
# handler.print_properties()
# handler.write_new_file('new.blend')

Java Class for .BLEND File Handling:
This class opens a .BLEND file, decodes the header properties, prints them to console, and includes a write method to create a minimal new .BLEND file with custom header.

import java.io.*;
import java.util.zip.GZIPInputStream;

public class BlendFileHandler {
    private String filepath;
    private String magic;
    private char ptrSizeId;
    private int ptrSize;
    private char endianId;
    private String endian;
    private String version;
    private boolean isCompressed;

    public BlendFileHandler(String filepath) {
        this.filepath = filepath;
        readAndDecode();
    }

    private void readAndDecode() {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            byte[] header = new byte[12];
            fis.read(header);
            if (header[0] == 0x1F && (header[1] & 0xFF) == 0x8B) {
                isCompressed = true;
                try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream(filepath))) {
                    gis.read(header);
                }
            }
            magic = new String(header, 0, 7, "ASCII");
            ptrSizeId = (char) header[7];
            ptrSize = ptrSizeId == '_' ? 4 : 8;
            endianId = (char) header[8];
            endian = endianId == 'v' ? "little-endian" : "big-endian";
            version = new String(header, 9, 3, "ASCII");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        System.out.println("Compression: " + (isCompressed ? "Gzip compressed" : "Uncompressed"));
        System.out.println("Magic String: " + magic);
        System.out.println("Pointer Size Indicator: " + ptrSizeId + " (" + ptrSize + " bytes)");
        System.out.println("Endianness Indicator: " + endianId + " (" + endian + ")");
        System.out.println("Version Number: " + version);
    }

    public void writeNewFile(String newFilepath, String version, int ptrSize, String endian, boolean compress) {
        char ptrSizeId = (ptrSize == 8) ? '-' : '_';
        char endianId = endian.equals("little") ? 'v' : 'V';
        byte[] header = ("BLENDER" + ptrSizeId + endianId + version).getBytes();
        try {
            if (compress) {
                try (GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream(newFilepath))) {
                    gos.write(header);  // Minimal; add blocks in real use
                }
            } else {
                try (FileOutputStream fos = new FileOutputStream(newFilepath)) {
                    fos.write(header);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     BlendFileHandler handler = new BlendFileHandler("example.blend");
    //     handler.printProperties();
    //     handler.writeNewFile("new.blend", "300", 8, "little", false);
    // }
}

JavaScript Class for .BLEND File Handling:
This class handles a .BLEND file via File object (e.g., from input), decodes the header properties, prints them to console, and includes a write method to create a minimal new .BLEND Blob for download.

class BlendFileHandler {
    constructor(file) {
        this.file = file;
        this.magic = null;
        this.ptrSizeId = null;
        this.ptrSize = null;
        this.endianId = null;
        this.endian = null;
        this.version = null;
        this.isCompressed = false;
        this.readAndDecode();
    }

    async readAndDecode() {
        const arrayBuffer = await this.file.arrayBuffer();
        const uint8Array = new Uint8Array(arrayBuffer.slice(0, 12));
        if (uint8Array[0] === 0x1F && uint8Array[1] === 0x8B) {
            this.isCompressed = true;
            // Decompression requires pako or similar; assume uncompressed for parsing here.
        }
        this.magic = String.fromCharCode(...uint8Array.slice(0, 7));
        this.ptrSizeId = String.fromCharCode(uint8Array[7]);
        this.ptrSize = this.ptrSizeId === '_' ? 4 : 8;
        this.endianId = String.fromCharCode(uint8Array[8]);
        this.endian = this.endianId === 'v' ? 'little-endian' : 'big-endian';
        this.version = String.fromCharCode(...uint8Array.slice(9, 12));
    }

    printProperties() {
        console.log(`Compression: ${this.isCompressed ? 'Gzip compressed' : 'Uncompressed'}`);
        console.log(`Magic String: ${this.magic}`);
        console.log(`Pointer Size Indicator: ${this.ptrSizeId} (${this.ptrSize} bytes)`);
        console.log(`Endianness Indicator: ${this.endianId} (${this.endian})`);
        console.log(`Version Number: ${this.version}`);
    }

    writeNewFile(version = '300', ptrSize = 8, endian = 'little', compress = false) {
        const ptrSizeId = ptrSize === 8 ? '-' : '_';
        const endianId = endian === 'little' ? 'v' : 'V';
        const header = `BLENDER${ptrSizeId}${endianId}${version}`;
        let blob = new Blob([header]);  // Minimal; add blocks in real use
        if (compress) {
            // Requires compression library like pako; skipped here.
        }
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'new.blend';
        a.click();
        URL.revokeObjectURL(url);
    }
}

// Example usage:
// const input = document.createElement('input');
// input.type = 'file';
// input.onchange = (e) => {
//     const handler = new BlendFileHandler(e.target.files[0]);
//     handler.printProperties();
//     handler.writeNewFile();
// };
// input.click();

C++ Class for .BLEND File Handling:
This class (using C++ for "class" support) opens a .BLEND file, decodes the header properties, prints them to console, and includes a write method to create a minimal new .BLEND file with custom header.

#include <iostream>
#include <fstream>
#include <string>
#include <zlib.h>  // For gzip check, but full decompress requires gzopen

class BlendFileHandler {
private:
    std::string filepath;
    std::string magic;
    char ptr_size_id;
    int ptr_size;
    char endian_id;
    std::string endian;
    std::string version;
    bool is_compressed;

public:
    BlendFileHandler(const std::string& fp) : filepath(fp), is_compressed(false) {
        read_and_decode();
    }

    void read_and_decode() {
        std::ifstream file(filepath, std::ios::binary);
        if (!file) return;
        char header[12];
        file.read(header, 12);
        if (header[0] == 0x1F && static_cast<unsigned char>(header[1]) == 0x8B) {
            is_compressed = true;
            // For full decompress, use gzFile; assume uncompressed for parsing here.
        }
        magic = std::string(header, 7);
        ptr_size_id = header[7];
        ptr_size = (ptr_size_id == '_') ? 4 : 8;
        endian_id = header[8];
        endian = (endian_id == 'v') ? "little-endian" : "big-endian";
        version = std::string(header + 9, 3);
    }

    void print_properties() {
        std::cout << "Compression: " << (is_compressed ? "Gzip compressed" : "Uncompressed") << std::endl;
        std::cout << "Magic String: " << magic << std::endl;
        std::cout << "Pointer Size Indicator: " << ptr_size_id << " (" << ptr_size << " bytes)" << std::endl;
        std::cout << "Endianness Indicator: " << endian_id << " (" << endian << ")" << std::endl;
        std::cout << "Version Number: " << version << std::endl;
    }

    void write_new_file(const std::string& new_filepath, const std::string& ver = "300", int ps = 8, const std::string& end = "little", bool comp = false) {
        char ps_id = (ps == 8) ? '-' : '_';
        char end_id = (end == "little") ? 'v' : 'V';
        std::string header = "BLENDER";
        header += ps_id;
        header += end_id;
        header += ver;
        if (comp) {
            // Use gzFile for compression; skipped for simplicity.
            std::ofstream out(new_filepath, std::ios::binary);
            out.write(header.c_str(), header.size());
        } else {
            std::ofstream out(new_filepath, std::ios::binary);
            out.write(header.c_str(), header.size());
        }
    }
};

// Example usage:
// int main() {
//     BlendFileHandler handler("example.blend");
//     handler.print_properties();
//     handler.write_new_file("new.blend");
//     return 0;
// }