Task 767: .VBR File Format

Task 767: .VBR File Format

1. List of Properties Intrinsic to the .VBR File Format

The .VBR file format, as used by GIMP for generated brushes, is a plain text format with fields separated by line breaks. The properties are derived from its structure and are as follows (note that some are conditional based on the version):

  • Magic string: A fixed identifier string, always "GIMP-VBR".
  • Version: The format version, either "1.0" (for non-shaped brushes) or "1.5" (for shaped brushes).
  • Name: The brush name, a UTF-8 encoded string with a maximum length of 255 bytes.
  • Shape: The brush shape, one of "circle", "square", or "diamond" (present only in version 1.5).
  • Spacing: The brush spacing, represented as a floating-point number.
  • Radius: The brush radius in pixels, represented as a floating-point number.
  • Spikes: The number of spikes for shaped brushes, represented as an integer (present only in version 1.5).
  • Hardness: The brush hardness, represented as a floating-point number.
  • Aspect ratio: The brush aspect ratio, represented as a floating-point number.
  • Angle: The brush angle, represented as a floating-point number.

These properties define the intrinsic structure of the file, with the total number of lines being 8 for version 1.0 and 10 for version 1.5.

The following are direct download links to sample .VBR files from publicly available GIMP brush repositories:

3. HTML/JavaScript for Drag-and-Drop .VBR File Dumper

The following is an embeddable HTML snippet with JavaScript that can be used in a blogging platform such as Ghost. It provides a drop zone where a user can drag and drop a .VBR file, parses the file, and displays all properties on the screen.

.VBR File Dumper
Drag and drop a .VBR file here

4. Python Class for .VBR File Handling

The following Python class can open, decode (read), encode (write), and print the properties of a .VBR file.

import os

class VBRFile:
    def __init__(self, filename=None):
        self.magic = 'GIMP-VBR'
        self.version = None
        self.name = None
        self.shape = None
        self.spacing = None
        self.radius = None
        self.spikes = None
        self.hardness = None
        self.aspect_ratio = None
        self.angle = None
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'r', encoding='utf-8') as f:
            lines = [line.strip() for line in f.readlines()]
        if len(lines) < 8 or lines[0] != 'GIMP-VBR':
            raise ValueError('Invalid .VBR file: Missing or incorrect magic string.')
        self.magic = lines[0]
        self.version = lines[1]
        self.name = lines[2]
        if self.version == '1.0':
            if len(lines) != 8:
                raise ValueError('Invalid .VBR file: Incorrect number of lines for version 1.0.')
            self.spacing = float(lines[3])
            self.radius = float(lines[4])
            self.hardness = float(lines[5])
            self.aspect_ratio = float(lines[6])
            self.angle = float(lines[7])
        elif self.version == '1.5':
            if len(lines) != 10:
                raise ValueError('Invalid .VBR file: Incorrect number of lines for version 1.5.')
            self.shape = lines[3]
            self.spacing = float(lines[4])
            self.radius = float(lines[5])
            self.spikes = int(lines[6])
            self.hardness = float(lines[7])
            self.aspect_ratio = float(lines[8])
            self.angle = float(lines[9])
        else:
            raise ValueError('Unsupported .VBR version.')

    def write(self, filename):
        if self.version not in ['1.0', '1.5']:
            raise ValueError('Version must be set to 1.0 or 1.5 before writing.')
        lines = [self.magic, self.version, self.name]
        if self.version == '1.5':
            if self.shape is None or self.spikes is None:
                raise ValueError('Shape and spikes must be set for version 1.5.')
            lines.extend([self.shape, str(self.spacing), str(self.radius), str(self.spikes),
                          str(self.hardness), str(self.aspect_ratio), str(self.angle)])
        else:
            if self.shape is not None or self.spikes is not None:
                raise ValueError('Shape and spikes should not be set for version 1.0.')
            lines.extend([str(self.spacing), str(self.radius), str(self.hardness),
                          str(self.aspect_ratio), str(self.angle)])
        with open(filename, 'w', encoding='utf-8') as f:
            f.write('\n'.join(lines) + '\n')

    def print_properties(self):
        print(f'Magic string: {self.magic}')
        print(f'Version: {self.version}')
        print(f'Name: {self.name}')
        if self.version == '1.5':
            print(f'Shape: {self.shape}')
        print(f'Spacing: {self.spacing}')
        print(f'Radius: {self.radius}')
        if self.version == '1.5':
            print(f'Spikes: {self.spikes}')
        print(f'Hardness: {self.hardness}')
        print(f'Aspect ratio: {self.aspect_ratio}')
        print(f'Angle: {self.angle}')

5. Java Class for .VBR File Handling

The following Java class can open, decode (read), encode (write), and print the properties of a .VBR file.

import java.io.*;
import java.util.Scanner;

public class VBRFile {
    private String magic = "GIMP-VBR";
    private String version;
    private String name;
    private String shape;
    private float spacing;
    private float radius;
    private Integer spikes;
    private float hardness;
    private float aspectRatio;
    private float angle;

    public VBRFile() {}

    public VBRFile(String filename) throws IOException {
        read(filename);
    }

    public void read(String filename) throws IOException {
        try (Scanner scanner = new Scanner(new File(filename), "UTF-8")) {
            String[] lines = new String[10];
            int count = 0;
            while (scanner.hasNextLine() && count < 10) {
                lines[count++] = scanner.nextLine().trim();
            }
            if (count < 8 || !lines[0].equals("GIMP-VBR")) {
                throw new IOException("Invalid .VBR file: Missing or incorrect magic string.");
            }
            this.magic = lines[0];
            this.version = lines[1];
            this.name = lines[2];
            if (version.equals("1.0")) {
                if (count != 8) {
                    throw new IOException("Invalid .VBR file: Incorrect number of lines for version 1.0.");
                }
                this.spacing = Float.parseFloat(lines[3]);
                this.radius = Float.parseFloat(lines[4]);
                this.hardness = Float.parseFloat(lines[5]);
                this.aspectRatio = Float.parseFloat(lines[6]);
                this.angle = Float.parseFloat(lines[7]);
            } else if (version.equals("1.5")) {
                if (count != 10) {
                    throw new IOException("Invalid .VBR file: Incorrect number of lines for version 1.5.");
                }
                this.shape = lines[3];
                this.spacing = Float.parseFloat(lines[4]);
                this.radius = Float.parseFloat(lines[5]);
                this.spikes = Integer.parseInt(lines[6]);
                this.hardness = Float.parseFloat(lines[7]);
                this.aspectRatio = Float.parseFloat(lines[8]);
                this.angle = Float.parseFloat(lines[9]);
            } else {
                throw new IOException("Unsupported .VBR version.");
            }
        }
    }

    public void write(String filename) throws IOException {
        if (version == null || (!version.equals("1.0") && !version.equals("1.5"))) {
            throw new IOException("Version must be set to 1.0 or 1.5 before writing.");
        }
        try (PrintWriter writer = new PrintWriter(new FileWriter(filename, java.nio.charset.StandardCharsets.UTF_8))) {
            writer.println(magic);
            writer.println(version);
            writer.println(name);
            if (version.equals("1.5")) {
                if (shape == null || spikes == null) {
                    throw new IOException("Shape and spikes must be set for version 1.5.");
                }
                writer.println(shape);
                writer.println(spacing);
                writer.println(radius);
                writer.println(spikes);
                writer.println(hardness);
                writer.println(aspectRatio);
                writer.println(angle);
            } else {
                if (shape != null || spikes != null) {
                    throw new IOException("Shape and spikes should not be set for version 1.0.");
                }
                writer.println(spacing);
                writer.println(radius);
                writer.println(hardness);
                writer.println(aspectRatio);
                writer.println(angle);
            }
        }
    }

    public void printProperties() {
        System.out.println("Magic string: " + magic);
        System.out.println("Version: " + version);
        System.out.println("Name: " + name);
        if (version.equals("1.5")) {
            System.out.println("Shape: " + shape);
        }
        System.out.println("Spacing: " + spacing);
        System.out.println("Radius: " + radius);
        if (version.equals("1.5")) {
            System.out.println("Spikes: " + spikes);
        }
        System.out.println("Hardness: " + hardness);
        System.out.println("Aspect ratio: " + aspectRatio);
        System.out.println("Angle: " + angle);
    }
}

6. JavaScript Class for .VBR File Handling

The following JavaScript class (intended for Node.js) can open, decode (read), encode (write), and print the properties of a .VBR file.

const fs = require('fs');

class VBRFile {
    constructor(filename = null) {
        this.magic = 'GIMP-VBR';
        this.version = null;
        this.name = null;
        this.shape = null;
        this.spacing = null;
        this.radius = null;
        this.spikes = null;
        this.hardness = null;
        this.aspectRatio = null;
        this.angle = null;
        if (filename) {
            this.read(filename);
        }
    }

    read(filename) {
        const content = fs.readFileSync(filename, 'utf-8');
        const lines = content.trim().split('\n');
        if (lines.length < 8 || lines[0] !== 'GIMP-VBR') {
            throw new Error('Invalid .VBR file: Missing or incorrect magic string.');
        }
        this.magic = lines[0];
        this.version = lines[1];
        this.name = lines[2];
        if (this.version === '1.0') {
            if (lines.length !== 8) {
                throw new Error('Invalid .VBR file: Incorrect number of lines for version 1.0.');
            }
            this.spacing = parseFloat(lines[3]);
            this.radius = parseFloat(lines[4]);
            this.hardness = parseFloat(lines[5]);
            this.aspectRatio = parseFloat(lines[6]);
            this.angle = parseFloat(lines[7]);
        } else if (this.version === '1.5') {
            if (lines.length !== 10) {
                throw new Error('Invalid .VBR file: Incorrect number of lines for version 1.5.');
            }
            this.shape = lines[3];
            this.spacing = parseFloat(lines[4]);
            this.radius = parseFloat(lines[5]);
            this.spikes = parseInt(lines[6]);
            this.hardness = parseFloat(lines[7]);
            this.aspectRatio = parseFloat(lines[8]);
            this.angle = parseFloat(lines[9]);
        } else {
            throw new Error('Unsupported .VBR version.');
        }
    }

    write(filename) {
        if (this.version !== '1.0' && this.version !== '1.5') {
            throw new Error('Version must be set to 1.0 or 1.5 before writing.');
        }
        let lines = [this.magic, this.version, this.name];
        if (this.version === '1.5') {
            if (this.shape === null || this.spikes === null) {
                throw new Error('Shape and spikes must be set for version 1.5.');
            }
            lines = lines.concat([this.shape, this.spacing, this.radius, this.spikes, this.hardness, this.aspectRatio, this.angle]);
        } else {
            if (this.shape !== null || this.spikes !== null) {
                throw new Error('Shape and spikes should not be set for version 1.0.');
            }
            lines = lines.concat([this.spacing, this.radius, this.hardness, this.aspectRatio, this.angle]);
        }
        fs.writeFileSync(filename, lines.join('\n') + '\n', 'utf-8');
    }

    printProperties() {
        console.log(`Magic string: ${this.magic}`);
        console.log(`Version: ${this.version}`);
        console.log(`Name: ${this.name}`);
        if (this.version === '1.5') {
            console.log(`Shape: ${this.shape}`);
        }
        console.log(`Spacing: ${this.spacing}`);
        console.log(`Radius: ${this.radius}`);
        if (this.version === '1.5') {
            console.log(`Spikes: ${this.spikes}`);
        }
        console.log(`Hardness: ${this.hardness}`);
        console.log(`Aspect ratio: ${this.aspectRatio}`);
        console.log(`Angle: ${this.angle}`);
    }
}

7. C++ Class for .VBR File Handling

The following C++ class can open, decode (read), encode (write), and print the properties of a .VBR file.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdexcept>
#include <sstream>

class VBRFile {
public:
    std::string magic = "GIMP-VBR";
    std::string version;
    std::string name;
    std::string shape;
    float spacing;
    float radius;
    int spikes = -1; // -1 indicates not set
    float hardness;
    float aspect_ratio;
    float angle;

    VBRFile() {}

    VBRFile(const std::string& filename) {
        read(filename);
    }

    void read(const std::string& filename) {
        std::ifstream file(filename);
        if (!file.is_open()) {
            throw std::runtime_error("Unable to open file.");
        }
        std::vector<std::string> lines;
        std::string line;
        while (std::getline(file, line)) {
            // Trim trailing whitespace, including \r if any
            line.erase(std::find_if(line.rbegin(), line.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), line.end());
            lines.push_back(line);
        }
        file.close();
        if (lines.size() < 8 || lines[0] != "GIMP-VBR") {
            throw std::runtime_error("Invalid .VBR file: Missing or incorrect magic string.");
        }
        magic = lines[0];
        version = lines[1];
        name = lines[2];
        if (version == "1.0") {
            if (lines.size() != 8) {
                throw std::runtime_error("Invalid .VBR file: Incorrect number of lines for version 1.0.");
            }
            spacing = std::stof(lines[3]);
            radius = std::stof(lines[4]);
            hardness = std::stof(lines[5]);
            aspect_ratio = std::stof(lines[6]);
            angle = std::stof(lines[7]);
            shape = "";
            spikes = -1;
        } else if (version == "1.5") {
            if (lines.size() != 10) {
                throw std::runtime_error("Invalid .VBR file: Incorrect number of lines for version 1.5.");
            }
            shape = lines[3];
            spacing = std::stof(lines[4]);
            radius = std::stof(lines[5]);
            spikes = std::stoi(lines[6]);
            hardness = std::stof(lines[7]);
            aspect_ratio = std::stof(lines[8]);
            angle = std::stof(lines[9]);
        } else {
            throw std::runtime_error("Unsupported .VBR version.");
        }
    }

    void write(const std::string& filename) {
        if (version != "1.0" && version != "1.5") {
            throw std::runtime_error("Version must be set to 1.0 or 1.5 before writing.");
        }
        std::ofstream file(filename);
        if (!file.is_open()) {
            throw std::runtime_error("Unable to open file for writing.");
        }
        file << magic << "\n";
        file << version << "\n";
        file << name << "\n";
        if (version == "1.5") {
            if (shape.empty() || spikes == -1) {
                throw std::runtime_error("Shape and spikes must be set for version 1.5.");
            }
            file << shape << "\n";
            file << spacing << "\n";
            file << radius << "\n";
            file << spikes << "\n";
            file << hardness << "\n";
            file << aspect_ratio << "\n";
            file << angle << "\n";
        } else {
            if (!shape.empty() || spikes != -1) {
                throw std::runtime_error("Shape and spikes should not be set for version 1.0.");
            }
            file << spacing << "\n";
            file << radius << "\n";
            file << hardness << "\n";
            file << aspect_ratio << "\n";
            file << angle << "\n";
        }
        file.close();
    }

    void print_properties() {
        std::cout << "Magic string: " << magic << std::endl;
        std::cout << "Version: " << version << std::endl;
        std::cout << "Name: " << name << std::endl;
        if (version == "1.5") {
            std::cout << "Shape: " << shape << std::endl;
        }
        std::cout << "Spacing: " << spacing << std::endl;
        std::cout << "Radius: " << radius << std::endl;
        if (version == "1.5") {
            std::cout << "Spikes: " << spikes << std::endl;
        }
        std::cout << "Hardness: " << hardness << std::endl;
        std::cout << "Aspect ratio: " << aspect_ratio << std::endl;
        std::cout << "Angle: " << angle << std::endl;
    }
};