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

> Explore how ArmorPaint serializes projects to the ARM file format. Discover the synergy between Python and C in the export pipeline for efficient project saving and loading.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: internals
- Published: 2026-09-14

---

**The ArmorPaint export pipeline combines Python-based project inspection with high-performance C binary serialization, where [`ioexportarm.py`](https://github.com/armory3d/armorpaint/blob/main/ioexportarm.py) marshals UI state into `ctypes` structures that [`boxexport.c`](https://github.com/armory3d/armorpaint/blob/main/boxexport.c) passes to [`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/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**.

- **[`ioexportarm.py`](https://github.com/armory3d/armorpaint/blob/main/ioexportarm.py)** (located in [`tools/ioexportarm.py`](https://github.com/armory3d/armorpaint/blob/main/tools/ioexportarm.py)): Traverses the active project state, converts Python objects into C-compatible structures, and manages the export workflow via `ctypes`.
- **[`boxexport.c`](https://github.com/armory3d/armorpaint/blob/main/boxexport.c)** (located in [`paint/tools/boxexport.c`](https://github.com/armory3d/armorpaint/blob/main/paint/tools/boxexport.c)): Acts as a thin C wrapper that receives marshaled data and forwards it to the core serialization engine.
- **[`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/iron_armpack.c)** (located in [`base/sources/iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/base/sources/iron_armpack.c)): Implements the low-level ARM specification, handling header generation, compression, and chunk-based storage.

## Native Binary Serialization in iron_armpack.c

At the heart of the export process, [`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/ioexportarm.py) calls `c_export.prepare_data()`, a `ctypes` wrapper around [`boxexport.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/boxexport.c) into an `ArmData` struct via `ctypes`.
4. **Binary Writing**: [`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/ioexportarm.py) employs:

```python
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`](https://github.com/armory3d/armorpaint/blob/main/ioexportarm.py)** manages the high-level workflow, extracting UI state and marshaling it to C via `ctypes`.
- **[`boxexport.c`](https://github.com/armory3d/armorpaint/blob/main/boxexport.c)** serves as the intermediary wrapper, translating Python structures for the native layer.
- **[`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/iron_armpack.c)) while leveraging Python's flexibility for UI integration and data introspection ([`ioexportarm.py`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/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`](https://github.com/armory3d/armorpaint/blob/main/tools/ioexportarm.py) traverses the UI layer tree and writes the hierarchy into the JSON manifest as nested dictionaries. When [`iron_armpack.c`](https://github.com/armory3d/armorpaint/blob/main/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.