How ArmorPaint Serializes Projects to the ARM File Format: Inside boxexport.c and ioexportarm.py

The ArmorPaint export pipeline combines Python-based project inspection with high-performance C binary serialization, where ioexportarm.py marshals UI state into ctypes structures that boxexport.c passes to iron_armpack.c to generate ARM archives containing a versioned header, JSON manifest, and LZ4-compressed data chunks.

The ARM file format is ArmorPaint's proprietary container for storing layered 3D painting projects, encapsulating everything from mask hierarchies to raw texture bytes. According to the armory3d/armorpaint source code, serialization relies on a hybrid two-stage architecture: Python scripts handle high-level state extraction and UI integration, while optimized C routines manage binary packing, compression, and file I/O. This separation balances the flexibility needed for rapid iteration against the throughput required for 4K and 8K texture exports.

Architecture of the ARM Export Pipeline

The pipeline divides responsibilities between a Python orchestrator and a native binary writer.

Native Binary Serialization in iron_armpack.c

At the heart of the export process, iron_armpack.c constructs the physical ARM archive using a fixed layout optimized for fast random access and storage efficiency.

ARM Container Structure

The file format begins with a 16-byte header containing magic bytes (ARM_MAGIC), version identifiers, and format flags. Immediately following the header, the writer emits a JSON manifest that describes the project structure—including layer names, blend modes, opacity values, and byte offsets for each data chunk. Finally, the file appends binary data chunks containing compressed texture and mask payloads.

Compression and Chunk Writing

Each texture or mask is compressed using LZ4 via lz4_compress before writing. The arm_export routine writes chunks sequentially with a standard header specifying type, size, and compressedSize, enabling the loader to seek directly to specific resources without decompressing the entire archive. This chunk-based approach minimizes memory pressure during both save and load operations.

The arm_export Entry Point

The function void arm_export(const char *name, ArmData *data) serves as the primary entry point defined in base/sources/iron_armpack.c (lines 45–78). This routine orchestrates header emission, manifest serialization through json_write, and the iterative compression of image buffers before flushing the final buffer to disk using the platform-agnostic iron_file_write API.

Python Orchestration in ioexportarm.py

The Python layer bridges the ArmorPaint interface with the native serialization engine, handling data conversion, memory safety, and error propagation.

Collecting Project State

The collect_project() function traverses the layer tree displayed in the UI to build a comprehensive Python dictionary. This structure contains layer metadata (names, blend modes, opacity), image data converted to raw byte arrays using NumPy, and auxiliary properties such as brush strokes and material assignments.

Marshaling Data via ctypes

To cross the language boundary, ioexportarm.py calls c_export.prepare_data(), a ctypes wrapper around boxexport.c. This conversion generates an ArmData pointer that the Python runtime can pass directly to the native arm_export function. The wrapper ensures that Python byte arrays are cast to ctypes.c_char_p or ctypes.POINTER(ctypes.c_ubyte) as required by the C API.

Memory Management and Error Handling

After arm_export returns, the Python script invokes c_export.free_arm_data(data_ptr) to release native memory, preventing leaks during batch exports. Errors captured during C execution are propagated back to the Python layer and reported to the user through the ArmorPaint interface.

Step-by-Step Serialization Flow

  1. UI Trigger: The user selects Export → ArmorPaint (.arm), executing tools/ioexportarm.py.
  2. Data Harvesting: collect_project() extracts the current layer stack, masks, and textures into a Python dictionary.
  3. C Structure Preparation: The dictionary is marshaled through boxexport.c into an ArmData struct via ctypes.
  4. Binary Writing: iron_armpack.c writes the 16-byte header, compresses the JSON manifest, and appends LZ4-compressed texture chunks.
  5. Finalization: The completed archive is flushed to the specified output path, and Python reports success or captures error codes.

Programmatic Export Example

The following Python snippet demonstrates how to invoke the native exporter manually using the same ctypes interface that ioexportarm.py employs:

import ctypes
import json
from pathlib import Path

# Load the compiled library produced from boxexport.c

lib = ctypes.CDLL(Path('build/libarmorpaint_export.so').as_posix())

class ArmData(ctypes.Structure):
    _fields_ = [
        ("manifest", ctypes.c_char_p),
        ("manifest_len", ctypes.c_int),
        ("textures", ctypes.POINTER(ctypes.c_ubyte)),
        ("textures_len", ctypes.c_int),
    ]

# Prepare project description

project = {
    "layers": [{"name": "Base", "opacity": 1.0, "blend": "mix"}],
    "textures": {"Base": Path("base.png").read_bytes()}
}
manifest = json.dumps(project).encode('utf-8')

data = ArmData()
data.manifest = ctypes.create_string_buffer(manifest)
data.manifest_len = len(manifest)

# Configure and call the export function

lib.arm_export.argtypes = [ctypes.c_char_p, ctypes.POINTER(ArmData)]
lib.arm_export(b'output.art', ctypes.byref(data))

# Cleanup native memory

lib.free_arm_data(ctypes.byref(data))

Summary

  • ARM files are composite archives comprising a fixed 16-byte header, JSON metadata, and compressed binary chunks.
  • ioexportarm.py manages the high-level workflow, extracting UI state and marshaling it to C via ctypes.
  • boxexport.c serves as the intermediary wrapper, translating Python structures for the native layer.
  • iron_armpack.c performs the actual binary serialization, using LZ4 compression for texture data and maintaining a chunk-based layout for efficient access.
  • The pipeline requires explicit memory management, with Python responsible for freeing native ArmData allocations after export via free_arm_data.

Frequently Asked Questions

What is the internal structure of an ARM file?

An ARM archive consists of three sequential sections: a 16-byte fixed header containing version and magic constants, a JSON manifest describing the project hierarchy and layer properties, and a series of compressed data chunks storing raw texture and mask bytes. This layout enables ArmorPaint to load metadata without decompressing heavy image data.

Why does ArmorPaint use Python and C for exports instead of a single language?

The hybrid approach isolates performance-critical binary operations in optimized C code (iron_armpack.c) while leveraging Python's flexibility for UI integration and data introspection (ioexportarm.py). This separation allows artists to export large 4K texture sets with native speed while maintaining a scriptable, extensible interface for custom workflows.

What compression algorithm does the ARM format use?

According to the implementation in base/sources/iron_armpack.c, texture and mask data is compressed using LZ4, chosen for its high decompression speed and low memory footprint. The JSON manifest may also be compressed depending on the version flags set in the ARM header.

How are layer hierarchies preserved during serialization?

The collect_project() function in tools/ioexportarm.py traverses the UI layer tree and writes the hierarchy into the JSON manifest as nested dictionaries. When iron_armpack.c serializes this manifest, it preserves order and nesting, allowing the loader to reconstruct the exact layer stack and blend relationships upon import.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →