grid-apps-cmms/docs/refs/MSLA_FORMAT_REFERENCE.md
2026-07-26 13:52:55 -04:00

1031 lines
35 KiB
Markdown

# mSLA File Format Reference
**Generated:** 2026-05-16
**Total Formats:** 31 registered in UVtools `AvailableFormats` (plus an unregistered CXDLPv1 implementation)
---
## Format Capability Matrix
| Format | Encode | Decode | Extensions | Versions | Profiles | RLE Type | Notes |
|--------|:------:|:------:|------------|----------|----------|----------|-------|
| **Chitubox Family** |
| ChituboxFile | ✓ | ✓ | photon, cbddlp, ctb, gktwo.ctb | 1-5 (def: 5) | No | RLE125 / CTB variable RLE | Most common |
| CTBEncryptedFile | ✓ | ✓ | ctb, encrypted.ctb | 4-5 (def: 5) | No | CTB variable RLE + AES | Encrypted |
| PHZFile | ✓ | ✓ | phz | 2 | No | RLE125 | Chitubox PHZ |
| ChituboxZipFile | ✓ | ✓ | zip | - | No | PNG | G-code based |
| AnycubicPhotonSFile | ✓ | ✓ | photons | - | No | RLE128 | Legacy Photon S |
| **Anycubic Family - Legacy (v1, v515-516)** |
| AnycubicFile (PWS) | ✓ | ✓ | pws | 1 | Yes (22) | 1-byte bit-plane RLE125 | PhotonS |
| AnycubicFile (PW0/PWX) | ✓ | ✓ | pw0, pwx | 1 | Yes (22) | Nibble RLE4 | PhotonZero, PhotonX |
| **Anycubic Family - Modern (v515-518)** |
| AnycubicFile (v515-v516) | ✓ | ✓ | dlp, pwmx, pwmo, pwms, pmsq, pwma, pm3, pm3m | varies | Yes (22) | Nibble RLE4 | Ultra, Mono, M3 family |
| AnycubicFile (v515-v517) | ✓ | ✓ | dl2p, pwmb, pmx2, pm3r | varies | Yes (22) | Nibble RLE4 | D2, X2, M3 Plus/Premium |
| AnycubicFile (v517) | ✓ | ✓ | pm3n, pm5, px6s | 517 | Yes (22) | Nibble RLE4 | Mono 2, X6Ks, M5 |
| AnycubicFile (v518) | ✓ | ✓ | pm5s, m5sp | 518 | Yes (22) | Nibble RLE4 | Mono M5s/Pro |
| AnycubicFile (unversioned fallback) | ✓ | ✓ | pm4n, pwc | 1, 515-518 | Yes (22) | Nibble RLE4 | Registered but not explicitly version-pinned |
| AnycubicZipFile | ✓ | ✓ | pm4u, pm7, pm7m, pwsz, pp1, pp1m | - | No | PNG | ZIP with JSON |
| **Creality Family** |
| CrealityCXDLPv1File | ✓ | ✓ | v1.cxdlp | 1 | No | RLE | Legacy implementation present but not registered in `AvailableFormats` |
| CrealityCXDLPFile | ✓ | ✓ | cxdlp | 2-3 (def: 3) | Yes | RLE | HALOT series |
| CrealityCXDLPv4File | ✓ | ✓ | cxdlpv4 | 4 | Yes | RLE | Current |
| **Other Binary Formats** |
| AnetFile | ✓ | ✓ | n4, n7 | 3 | Yes (N4/N7) | RLE | Anet printers |
| FDGFile | ✓ | ✓ | fdg | 2 | No | RLE | Voxelab |
| GooFile | ✓ | ✓ | goo, prz | - | No | Chunked RLE + checksum | Elegoo/Phrozen |
| LGSFile | ✓ | ✓ | lgs, lgs30, lgs120, lgs4k | - | Yes | Struct | Longer Orange |
| MDLPFile | ✓ | ✓ | mdlp | 1 | No | Vector | Makerbase MKS |
| GR1File | ✓ | ✓ | gr1 | - | No | Vector | GR Workshop |
| OSFFile | ✓ | ✓ | osf | 4 (def) | No | Compressed | Vlare |
| OSLAFile | ✓ | ✓ | osla | - | No | Varies | Open SLA |
| **Archive Formats** |
| SL1File | ✓ | ✓ | sl1, sl1s | - | Yes | PNG | Prusa (INI) |
| UVJFile | ✓ | ✓ | uvj | - | No | PNG | Vendor-neutral |
| VDAFile | ✓ | ✓ | zip | - | No | Images | Voxeldance Additive |
| VDTFile | ✓ | ✓ | vdt | 1 | No | PNG | Voxeldance Tango |
| NanoDLPFile | ✓ | ✓ | nanodlp, rgb.nanodlp | 1-2 | No | Images | NanoDLP |
| KlipperFile | ✓ | ✓ | zip, rgb.zip | - | No | PNG | Klipper firmware |
| ZCodeFile | ✓ | ✓ | zcode | - | No | Images | UnizMaker IBEE |
| ZCodexFile | ✓ | ✓ | zcodex | - | Yes | Images | Zortrax |
| CWSFile | ✓ | ✓ | cws, rgb.cws, xml.cws | - | No | PNG | NovaMaker/CW |
| JXSFile | ✓ | ✓ | jxs | - | No | Images | Uniformation |
| GenericZIPFile | ✓ | ✓ | zip | - | No | Varies | Fallback |
| **Text/Vector Formats** |
| FlashForgeSVGXFile | ✓ | ✓ | svgx | - | No | SVG | FlashForge |
| QDTFile | ✓ | ✓ | qdt | - | No | Text | Emake3D Galaxy |
| **Import Only** |
| ImageFile | ✓ | ✓ | png, jpg, jpeg, jp2, tga, tif, tiff, bmp, pbm | - | No | N/A | Image import |
**Total Extensions:** 60+
**Total Printer Profiles:** 7 formats with profiles (30+ printer models)
---
## Table of Contents - Corrected Binary Formats
### 1. Chitubox Family
#### 1.1 CBDDLP (Chitubox v1-2)
**Extensions:** `.cbddlp`, `.photon`
**Magic:** `0x12FD0019` (LE)
**Versions:** 1, 2
**RLE:** RLE125 (limit: 125 pixels)
**Header Structure (CORRECTED - 32-bit floats):**
```
Offset | Size | Type | Field | Notes
-------|------|--------|--------------------------|------------------
0x00 | 4 | uint32 | Magic | 0x12FD0019 (LE)
0x04 | 4 | uint32 | Version | 1 or 2
0x08 | 4 | float | BedSizeX | mm (32-bit)
0x0C | 4 | float | BedSizeY | mm (32-bit)
0x10 | 4 | float | BedSizeZ | mm (32-bit)
0x14 | 4 | uint32 | Unknown1 |
0x18 | 4 | uint32 | Unknown2 |
0x1C | 4 | float | TotalHeightMillimeter | mm (32-bit)
0x20 | 4 | float | LayerHeightMillimeter | mm (32-bit)
0x24 | 4 | float | LayerExposureSeconds | seconds (32-bit)
0x28 | 4 | float | BottomExposureSeconds | seconds (32-bit)
0x2C | 4 | float | LightOffDelay | seconds (32-bit)
0x30 | 4 | uint32 | BottomLayersCount |
0x34 | 4 | uint32 | ResolutionX | pixels
0x38 | 4 | uint32 | ResolutionY | pixels
0x3C | 4 | uint32 | PreviewLargeOffsetAddress |
0x40 | 4 | uint32 | LayersDefinitionOffsetAddress |
0x44 | 4 | uint32 | LayerCount |
0x48 | 4 | uint32 | PreviewSmallOffsetAddress |
0x4C | 4 | uint32 | PrintTime | seconds
0x50 | 4 | uint32 | ProjectorType | 0=Normal
0x54 | 4 | uint32 | PrintParametersOffsetAddress | (v2+)
0x58 | 4 | uint32 | PrintParametersSize | bytes (v2+)
0x5C | 4 | uint32 | AntiAliasLevel | 1 (v2+)
0x60 | 2 | uint16 | LightPWM | 0-255 (v2+)
0x62 | 2 | uint16 | BottomLightPWM | 0-255 (v2+)
```
**Header Size:** 0x64 (100 bytes) for v2
#### 1.2 CTB (Chitubox v3)
**Extensions:** `.ctb`
**Magic:** `0x12FD0086` (LE)
**Version:** 3
**RLE:** CTB variable-length 7-bit grayscale RLE, optionally XOR-encrypted with a per-layer seed-derived stream
**Header Structure (CORRECTED - matches Chitubox 1.8 fixtures):**
```
Offset | Size | Type | Field | Notes
-------|------|--------|--------------------------|------------------
0x00 | 4 | uint32 | Magic | 0x12FD0086 (LE)
0x04 | 4 | uint32 | Version | 3
0x08 | 4 | float | BedSizeX | mm (32-bit float)
0x0C | 4 | float | BedSizeY | mm (32-bit float)
0x10 | 4 | float | BedSizeZ | mm (32-bit float)
0x14 | 4 | uint32 | Unknown1 |
0x18 | 4 | uint32 | Unknown2 |
0x1C | 4 | float | TotalHeightMillimeter | mm (32-bit float)
0x20 | 4 | float | LayerHeightMillimeter | mm (32-bit float)
0x24 | 4 | float | LayerExposureSeconds | seconds (32-bit float)
0x28 | 4 | float | BottomExposureSeconds | seconds (32-bit float)
0x2C | 4 | float | LightOffDelay | seconds (32-bit float)
0x30 | 4 | uint32 | BottomLayersCount |
0x34 | 4 | uint32 | ResolutionX | pixels
0x38 | 4 | uint32 | ResolutionY | pixels
0x3C | 4 | uint32 | PreviewLargeOffsetAddress |
0x40 | 4 | uint32 | LayersDefinitionOffsetAddress |
0x44 | 4 | uint32 | LayerCount |
0x48 | 4 | uint32 | PreviewSmallOffsetAddress |
0x4C | 4 | uint32 | PrintTime | seconds
0x50 | 4 | uint32 | ProjectorType | 0=Normal
0x54 | 4 | uint32 | PrintParametersOffsetAddress |
0x58 | 4 | uint32 | PrintParametersSize | bytes
0x5C | 4 | uint32 | AntiAliasLevel | 1, 2, 4, 8
0x60 | 2 | uint16 | LightPWM | 0-255
0x62 | 2 | uint16 | BottomLightPWM | 0-255
0x64 | 4 | uint32 | EncryptionSeed | 0 disables layer RLE XOR
0x68 | 4 | uint32 | SlicerOffsetAddress |
0x6C | 4 | uint32 | SlicerDataSize | bytes
```
**Header Size:** 0x70 (112 bytes)
**CRITICAL NOTE:** Previous documentation incorrectly showed 8-byte (double) floats. CTB v3 uses 32-bit floats throughout. This matches Chitubox 1.8 fixtures and actual UVtools implementation.
**CTB Variable RLE Encoding (v3+)**
CTB v3 does not use fixed 2-byte `(color, length)` RLE. It uses a 7-bit grayscale, variable-length run format:
```
Color byte:
bit 7 clear: single pixel, bits 0-6 are grayscale level
bit 7 set: run follows, bits 0-6 are grayscale level
Run length bytes:
0rrrrrrr = 7-bit length, 2-127
10rrrrrr rrrrrrrr = 14-bit length, 128-16383
110rrrrr rrrrrrrr rrrrrrrr = 21-bit length
1110rrrr rrrrrrrr rrrrrrrr rrrrrrrr = 28-bit length
```
8-bit pixels are quantized with `grey7 = pixel >> 1`. When decoding, `grey7 == 0` maps to `0`; otherwise expand with `(grey7 << 1) | 1`, so encoded `0x7F` returns `0xFF`.
If the header encryption seed at `0x64` is non-zero, layer RLE bytes are XORed with a per-layer key stream derived from that seed. The seed is not the direct XOR key.
**CTB Run Examples:**
```
Input: 1 white pixel (255)
Encoded: 7F
Input: 100 white pixels (255)
Encoded: FF 64
Input: 500 gray pixels (128)
Encoded: C0 81 F4
Input: 20000 pixels at value 200
Encoded: E4 C0 4E 20
```
**CTB Layer XOR Key Schedule:**
```c
init = seed * 0x2D83CDAC + 0xD8A83423
key = (layerIndex * 0x1E1530CD + 0xEC3D47CD) * init
for each RLE byte:
xor with byte key[index]
after 4 bytes: key += init
```
CTB grayscale anti-aliasing stores 128 levels: `0x00` is masked, `0x7F` expands to `0xFF`, and intermediate values represent partial exposure.
#### 1.3 CTB v4-5 (Chitubox v4-5)
**Extensions:** `.ctb`, `.gktwo.ctb`
**Magic:** `0x12FD0106` (LE, v4), `0xFF220810` (LE, GKtwo variant)
**Versions:** 4, 5 (default: 5)
**RLE:** CTB variable-length 7-bit grayscale RLE
Same header structure as CTB v3, with additional support for:
- Per-layer exposure times
- Per-layer lift heights/speeds
- PageNumber field in layer table
- Transition layers
#### 1.4 CTB Encrypted v4-5
**Extensions:** `.ctb`, `.encrypted.ctb`
**Magic:** `0x12FD0107` (LE)
**Versions:** 4, 5 (default: 5)
**Settings block:** 288-byte slicer-settings table encrypted with AES-256-CBC, no padding, zero-padded to 16-byte blocks
**Layer data:** CTB variable-length 7-bit grayscale RLE, then CTB layer XOR using `LayerXorKey` (`0xEFBEADDE` by default); some files additionally AES-encrypt layer-data subranges
Encrypted CTB is a different container from plain CTB v3/v4/v5, not just a flag on the v3 layout. The fixed 48-byte header points at the encrypted settings block and a final encrypted SHA-256 signature. The AES key/IV are derived from UVtools-compatible Chitubox constants with the `UVtools` XOR mask. Kiri keeps these primitives native in JS in `x_ctb_crypto.js`; full writer support still needs the encrypted settings table, layer pointer table, resin parameters, previews, and final signature wired into `x_ctb`.
Key tables:
- File header: 48 bytes
- Slicer settings: 288 bytes
- Layer pointer: 16 bytes
- Layer definition: 88 bytes
- Per-layer settings flag: `0x40` allow, `0x00` disallow
### 2. Anycubic Family
#### 2.1 Legacy Anycubic Formats (v1)
##### 2.1.1 PWS (Photon / Photon S)
**Extensions:** `.pws`
**Version:** 1
**RLE:** RLE1 (limit: 125)
**Machines:** PhotonS
**RLE1 Encoding (PWS):**
```
For each run (1 byte):
bit 7 = white/on flag
bits 0-6 = repeat count
Runs are emitted per anti-aliasing bit-plane and then accumulated back to
8-bit grayscale. UVtools' encoder splits runs at 0x7D (125), although the
stored count field is 7 bits.
Constant: RLE1EncodingLimit = 0x7D (125)
```
##### 2.1.2 PW0/PWX (Photon Zero / Photon X) - CORRECTED
**Extensions:** `.pw0`, `.pwx`
**Version:** 1
**RLE:** Nibble-coded RLE4 (limit: 4095)
**Machines:** PhotonZero, PhotonX
**Nibble-Coded RLE4 Encoding (CORRECTED from UVtools implementation):**
The PW0 format uses a **nibble-coded variable-length RLE**, NOT a simple 3-byte RLE as previously documented.
**Encoding Rules:**
```
Each byte is split: [4-bit code] [4-bit repeat]
Code interpretation:
0x0: Black (0x00) - Extended format (2 bytes total)
Format: [0x0R] [RR]
Repeat = (R << 8) | RR (12-bit value, max 4095)
0xF: White (0xFF) - Extended format (2 bytes total)
Format: [0xFR] [RR]
Repeat = (R << 8) | RR (12-bit value, max 4095)
0x1-0xE: Grayscale - Single byte format
Color = (code << 4) | code
Repeat = 4-bit repeat value (1-15)
Examples:
0x17 = color 0x11, repeat 7
0x82 = color 0x88, repeat 2
```
**Decode Algorithm (from UVtools):**
```c
for (int i = 0; i < encodedRle.Length; i++)
{
byte b = encodedRle[i];
int code = b >> 4; // Upper nibble
int repeat = b & 0xf; // Lower nibble
byte color;
switch (code)
{
case 0x0: // Black (extended)
color = 0;
i++;
repeat = (repeat << 8) + encodedRle[i];
break;
case 0xf: // White (extended)
color = 255;
i++;
repeat = (repeat << 8) + encodedRle[i];
break;
default: // Grayscale (single byte)
color = (byte)((code << 4) | code);
break;
}
// Fill repeat pixels with color
}
```
**Encode Algorithm (from UVtools):**
```c
for each pixel:
int color = pixel >> 4; // Extract upper nibble
if (color == 0 or color == 0xf): // Black or white
if (repeat > 4095):
repeat = 4095
// Encode as 2 bytes: [cR] [RR]
ushort more = (ushort)(repeat | (color << 12));
output.Add((byte)(more >> 8));
output.Add((byte)more);
else: // Grayscale
if (repeat > 15):
repeat = 15
// Encode as 1 byte: [cR]
output.Add((byte)(repeat | color << 4));
```
**Constants:**
```c
RLE4EncodingLimit = 0xFFF = 4095
```
**Example Encodings:**
```
White run of 1000 pixels:
Binary: 1111 0011 1110 1000
Hex: F3 E8
Decode: code=0xF, repeat=3, next byte=0xE8
repeat = (3 << 8) + 232 = 1000
color = 255
Black run of 500 pixels:
Binary: 0000 0001 1111 0100
Hex: 01 F4
Decode: code=0x0, repeat=1, next byte=0xF4
repeat = (1 << 8) + 244 = 500
color = 0
Gray (0x77) run of 10 pixels:
Binary: 0111 1010
Hex: 7A
Decode: code=0x7, repeat=0xA (10)
color = (7 << 4) | 7 = 0x77 = 119
Gray (0xAA) run of 5 pixels:
Binary: 1010 0101
Hex: A5
Decode: code=0xA, repeat=0x5 (5)
color = (10 << 4) | 10 = 0xAA = 170
```
**Key Differences from Previous Documentation:**
- NOT a fixed 3-byte format
- Variable length: 1 byte for grayscale (repeat ≤15), 2 bytes for black/white
- Nibble-based encoding optimizes for common cases
- Color is encoded in upper nibble, duplicated to both nibbles for final value
- Maximum repeat: 4095 for black/white, 15 for grayscale
#### 2.2 Modern Anycubic Formats (v515-518)
All modern Anycubic formats use **Nibble-coded RLE4** encoding.
##### 2.2.1 Version-Mapped Formats Before v518
**Extension version sets from UVtools `GetAvailableVersionsForExtension`:**
- `.pwmx`, `.pwmo`, `.pwms`, `.pmsq`, `.dlp`: v1, v515, v516
- `.pwma`, `.pm3`, `.pm3m`: v515, v516
- `.pwmb`, `.dl2p`, `.pmx2`, `.pm3r`: v515, v516, v517
**Machines:**
- PhotonMono, PhotonMonoX, PhotonMonoX2
- PhotonMono4K, PhotonMonoSE, PhotonMonoSQ
- PhotonMonoX6K, PhotonM3, PhotonM3Plus, PhotonM3Max, PhotonM3Premium
- PhotonUltra, PhotonD2
**Features (v515+):**
- Nibble-coded RLE4 encoding
- Enhanced print parameters
- Per-layer settings support
**Features (v516+):**
- Dual lift/retract speeds
- Advanced motion profiles
##### 2.2.2 Version 517 Formats
**Extensions:** `.pm3n`, `.pm5`, `.px6s` (`.pm4n` is registered by UVtools but not explicitly version-pinned; treat v517 as an assumption until fixture-verified)
**Machines:**
- PhotonMono2 (.pm3n)
- PhotonMonoM5 (.pm5)
- PhotonMonoX6Ks (.px6s)
- PhotonMono4 (.pm4n) is mapped as a machine, but its source and PrusaSlicer profile omit an explicit file version
**New Features:**
- `SOFTWARE` and `MODEL` sections compared with earlier versions
- `MACHINE` property field count rises to 7
- Thumbnail defaults are 224x168 and 330x190; v517 writes the first `PREVIEW` table
- `HEADER` table length: 92 bytes
- `LayerDef` entries remain 32 bytes each
##### 2.2.3 Version 518 Formats
**Extensions:** `.pm5s`, `.m5sp` (`.pwc` is registered as Anycubic Custom Machine but falls through to all available versions)
**Machines:**
- PhotonMonoM5s (.pm5s)
- PhotonMonoM5sPro (.m5sp)
**New Features:**
- 11 FileMark table entries
- second `PREVIEW2` table may be present
- Preview sizes: 224x168, 330x190
- `HEADER` table length: 96 bytes
- `LayerDef` entries remain 32 bytes each; `SUBIMGS` sublayer records may also be present
- `MACHINE` property field count rises to 15
**Anycubic FileMark and Section Table (v515+):**
```
Offset | Size | Type | Field | Notes
-------|------|--------|--------------------------|------------------
0x00 | 12 | char[] | Mark | "ANYCUBIC" null-padded
0x0C | 4 | uint32 | Version | 1, 515, 516, 517, 518
0x10 | 4 | uint32 | NumberOfTables |
0x14 | 4 | uint32 | HeaderAddress | offset to HEADER table
0x18 | 4 | uint32 | SoftwareAddress | v517+
0x1C | 4 | uint32 | PreviewAddress |
0x20 | 4 | uint32 | LayerImageColorTableAddress |
0x24 | 4 | uint32 | LayerDefinitionAddress |
0x28 | 4 | uint32 | ExtraAddress | v516+
0x2C | 4 | uint32 | MachineAddress | v516+
0x30 | 4 | uint32 | LayerImageAddress |
0x34 | 4 | uint32 | ModelAddress | v517+
0x38 | 4 | uint32 | SubLayerDefinitionAddress| v518+
0x3C | 4 | uint32 | Preview2Address | v518+
```
Each section starts with a 12-byte null-padded table name followed by a 32-bit table length. The `HEADER` table length is version-dependent: 80 for early versions, 84 for v516, 92 for v517, and 96 for v518.
**Machine Detection (Extension → Machine mapping):**
```
.pm3n → PhotonMono2
.pm4n → PhotonMono4 (version ambiguous in UVtools; v517 is an implementation assumption pending fixtures)
.pm5 → PhotonMonoM5
.pm5s → PhotonMonoM5s
.m5sp → PhotonMonoM5sPro
.px6s → PhotonMonoX6Ks
.pwms → PhotonMonoSE
.pwma → PhotonMono4K
.pwmx → PhotonMonoX
.pwmo → PhotonMono
.pwmb → PhotonMonoX6K / PhotonM3Plus
```
**Photon Mono 4 / `.pm4n` Validation Notes:**
- UVtools registers `.pm4n` and maps it to `PhotonMono4`, but `GetAvailableVersionsForExtension()` falls through to all Anycubic versions.
- The bundled PrusaSlicer profile declares `FILEFORMAT_PM4N` but does not include `FILEVERSION_517`.
- Treat v517 output as a reasonable implementation assumption, not a source-verified fact, until a real Photon Mono 4 fixture confirms it.
- Fixture validation should verify `ANYCUBIC` at offset `0x00`, version at offset `0x0C`, `HEADER` table length, 32-byte `LayerDef` entries, and whether `SUBIMGS` / `PREVIEW2` are present.
**Anycubic Version Pattern:**
| Extension | Machine | Version in UVtools |
|-----------|---------|--------------------|
| `.pm3n` | PhotonMono2 | 517 |
| `.pm4n` | PhotonMono4 | fallback: 1, 515, 516, 517, 518 |
| `.pm5` | PhotonMonoM5 | 517 |
| `.pm5s` | PhotonMonoM5s | 518 |
#### 2.3 Anycubic ZIP Format (PWSZ)
**Extensions:** `.pm4u`, `.pm7`, `.pm7m`, `.pwsz`, `.pp1`, `.pp1m`
**Type:** ZIP archive with JSON manifest
**Layer Format:** PNG images in staged directories
**Structure:**
- `manifest` - JSON with version, machine_type, machine_extern
- `bott_0/*.png`, `bott_1/*.png` - Bottom stage layers
- `normal_0/*.png`, `normal_1/*.png` - Normal stage layers
---
## RLE Encoding Comparison
| Format | Name | Bytes/Run | Max Repeat | Color Bits | Notes |
|--------|------|-----------|------------|------------|-------|
| RLE1/RLE125 (PWS/CBDDLP) | Bit-plane run-length | 1 (fixed) | 125 encoded / 127 stored | 1 per AA plane | bit 7 on/off, low 7 bits repeat |
| RLE4 (PW0) | Nibble-coded | 1-2 (variable) | 4095 (B/W), 15 (gray) | 8 | Optimized variable-length |
| CTB variable RLE | 7-bit grayscale variable RLE | 1-5 (variable) | 268,435,455 | 7 stored / 8 expanded | CTB v3+ layer data |
| RLE128 (PhotonS) | Run-Length 128 | 2 (fixed) | 128 | 8 | Length, Color (BE) |
| GOO RLE | Chunked | 1-5 (+ checksum) | 268435455 | 8 | 0x55 stream + inverted checksum, layer CRLF-delimited |
**Performance Comparison:**
- **RLE1/RLE125/RLE128:** Simple, predictable, easy to implement, but PWS/CBDDLP are bit-plane encodings rather than full grayscale runs
- **CTB variable RLE:** Better compression and grayscale AA, plus optional seed-derived XOR
- **Nibble RLE4:** Best compression for grayscale AA, complex encoding
- **GOO RLE:** High max repeat with grayscale support and a per-layer checksum; CRLF only delimits the encoded payload
---
## Additional Binary Format Details
### Photon S (Anycubic Legacy)
**Extension:** `.photons`
**Magic:** TAG1=2, TAG2=49 (both big-endian)
**RLE:** RLE128 (limit: 128, big-endian)
**Header (Big-Endian):**
```
Offset | Size | Type | Field | Endianness
-------|------|--------|--------------------------|------------
0x00 | 4 | uint32 | TAG1 | 2 (BE)
0x04 | 4 | uint32 | TAG2 | 49 (BE)
0x08 | 8 | double | XYPixelSize | mm (BE)
0x10 | 4 | float | LayerHeight | mm (BE)
0x14 | 4 | float | ExposureTime | seconds (BE)
0x18 | 4 | float | ExposureTimeBottom | seconds (BE)
...
```
**RLE128 Encoding (Big-Endian):**
```
For each run (2 bytes):
byte[0] = length (1-128, BE)
byte[1] = color (0x00 or 0xFF)
```
### GOO (Elegoo)
**Extensions:** `.goo`, `.prz`
**Magic:** "V3.0" + `0x07000000` (BE) + "DLP\0"
**RLE:** Chunked stream with checksum. The layer payload is followed by a CRLF delimiter, but CRLF is not part of the RLE chunk grammar.
**RLE Format:**
```
Layer payload:
0x55 layer RLE magic
chunks... chunk stream
checksum bitwise NOT of byte sum from chunks
Chunk byte0:
bits 7:6 type
0 = black run (0x00)
1 = grayscale run; gray byte follows byte0
2 = delta from previous pixel
3 = white run (0xFF)
bits 5:4 length encoding for non-delta chunks
0 = byte0 low nibble
1 = next byte + low nibble
2 = next two bytes + low nibble
3 = next three bytes + low nibble
bits 3:0 low length nibble or delta value
Non-delta maximum repeat: 0x0FFFFFFF pixels.
Layer record delimiter after payload: 0x0D 0x0A.
```
**Tilt-vat motion:** Mars 5 Ultra and Saturn 4 Ultra GOO files are treated as tilt-vat machines. UVtools identifies them by machine name and substitutes tiny synthetic motion values before encode: lift height `0.05`, lift speed `0.05`, retract height `0`, retract speed `0.05`, with about 4 seconds of motor time in print-time estimates. Kiri models this as `sla_motion: "tilt"` on the device profile instead of overloading user peel distance/speed fields.
### CXDLP (Creality)
**Extension:** `.cxdlp`
**Magic:** "CXSW3DV2" (9 bytes, BE)
**Versions:** 2, 3, 4
**RLE:** Standard RLE
**Header (v3):**
```
Offset | Size | Type | Field | Endianness
-------|------|--------|--------------------------|------------
0x00 | 4 | uint32 | MagicSize | 9 (BE)
0x04 | 9 | char[] | MagicName | "CXSW3DV2" (BE)
0x0D | 4 | uint32 | Version | 3 (BE)
0x11 | 4 | uint32 | PrinterModel | offset (BE)
0x15 | 4 | uint32 | ResolutionX | pixels (BE)
0x19 | 4 | uint32 | ResolutionY | pixels (BE)
0x1D | 256 | uint32[]| LayerOffsets[64] | offsets (BE)
...
```
---
## Archive Format Structures
### SL1 (Prusa)
**Extensions:** `.sl1`, `.sl1s`
**Type:** ZIP with INI config
**Layer Format:** PNG images
**Required Files:**
- `config.ini` - Print parameters
- `prusaslicer.ini` - Slicer settings (optional)
- `00000.png`, `00001.png`, ... - Layers (5-digit zero-padded)
**config.ini Sample:**
```ini
[general]
expTime = 8.0
expTimeFirst = 35.0
layerHeight = 0.05
numFade = 10
numSlow = 5
printTime = 3600
[printer]
PrinterModel = SL1
```
### UVJ (Vendor-Neutral)
**Extension:** `.uvj`
**Type:** ZIP with JSON config
**Layer Format:** PNG images
**Structure:**
- `config.json` - Print configuration
- `slice/0.png`, `slice/1.png`, ... - Layer images
- `preview/huge.png` - Large preview
- `preview/tiny.png` - Small preview
**config.json Schema:**
```json
{
"size": {
"x": 2560,
"y": 1620,
"layerHeight": 0.05,
"millimeter": {"x": 192.0, "y": 120.0}
},
"exposure": {
"lightOnTime": 8.0,
"lightOffTime": 1.0,
"lightPWM": 255,
"liftHeight": 5.0,
"liftSpeed": 100.0,
"retractSpeed": 150.0
},
"bottom": {
"count": 5,
"lightOnTime": 35.0,
"lightPWM": 255,
...
},
"layers": [
{"z": 0.05},
{"z": 0.10},
...
]
}
```
### RSLA / VSLA (Grid.Space Generic)
**Extensions:** `.rsla`, `.vsla`
**Type:** ZIP with JSON manifest
**Layer Format:** RSLA stores raster PNG layers; VSLA stores SVG-like vector layer geometry
These formats are intended as a vendor-neutral interchange and debugging target. They carry enough process metadata to preserve behavior before conversion to a proprietary printer format.
**Shared manifest process fields:**
```json
{
"process": {
"bottomLayers": 5,
"transitionLayers": 8,
"exposure": 2.4,
"bottomExposure": 28,
"lightOffDelay": 0,
"bottomLightOffDelay": 0,
"waitBeforeCure": 0.5,
"bottomWaitBeforeCure": 2,
"waitAfterCure": 0,
"waitAfterLift": 0,
"liftHeight": 5,
"bottomLiftHeight": 6,
"liftSpeed": 180,
"bottomLiftSpeed": 60,
"retractHeight": 5,
"retractSpeed": 180,
"lightPWM": 255,
"bottomLightPWM": 255,
"motion": "normal",
"antiAlias": 1
}
}
```
Each layer also carries a resolved per-layer `process` object with `bottom`, `transition`, `transitionIndex`, effective exposure, waits, motion, and PWM. Proprietary writers should prefer this resolved per-layer model where their format supports it, then fall back to global process fields where it does not.
VSLA layer SVG roots additionally include process data attributes such as `data-exposure`, `data-light-off-delay`, `data-light-pwm`, `data-bottom`, and `data-transition` so the per-layer intent is visible without parsing the manifest.
---
## Implementation Guidance for Sandboxed Environments
### Recommended Implementation Order
**Tier 1 - Simplest (Start Here):**
1. **ImageFile** - Single image, no container
2. **UVJ** - JSON + PNG (if JSON/ZIP available)
3. **SL1** - INI + PNG (if INI/ZIP available)
**Tier 2 - Binary Formats:**
1. **CBDDLP v1** - Simplest binary RLE
2. **CTB v3** - Modern standard, widely used
3. **PWS** - Simple RLE1 encoding
**Tier 3 - Advanced Binary:**
1. **PW0** - Nibble-coded RLE4 (complex but efficient)
2. **PhotonS** - Big-endian RLE128
3. **GOO** - Chunked RLE with layer checksum
**Tier 4 - Specialized:**
1. **CTBEncrypted** - Requires AES-256-CBC
2. **AnycubicFile v517+** - Modern features, complex header
3. **CXDLP** - Big-endian offsets
### Essential Algorithms (Pseudocode)
#### RLE125 Bit-Plane Decoder (CBDDLP/PWS)
```python
def decode_rle125_bitplanes(data: bytes, width: int, height: int, aa: int = 1) -> bytes:
levels = bytearray(width * height)
data_index = 0
for _plane in range(aa):
pixel_index = 0
while pixel_index < len(levels):
code = data[data_index]
data_index += 1
length = code & 0x7F
if code & 0x80:
for i in range(length):
levels[pixel_index + i] += 1
pixel_index += length
pixels = bytearray(len(levels))
scale = 256 // aa
for i, level in enumerate(levels):
value = level * scale
pixels[i] = value - 1 if value else 0
return pixels
```
#### RLE125 Bit-Plane Encoder (Single Plane)
```python
def encode_rle125_bitplane(bits: list[bool]) -> bytes:
result = bytearray()
run_on = False
run = 0
def flush():
nonlocal run
if run:
result.append(run | (0x80 if run_on else 0))
run = 0
for bit in bits:
if bit == run_on and run < 0x7D:
run += 1
continue
flush()
run_on = bit
run = 1
flush()
return result
```
#### Nibble-Coded RLE4 Decoder (PW0)
```python
def decode_rle4_nibble(data: bytes, width: int, height: int) -> bytes:
pixels = bytearray(width * height)
pixel_pos = 0
i = 0
while i < len(data):
b = data[i]
code = b >> 4 # Upper nibble
repeat = b & 0x0F # Lower nibble
if code == 0x0: # Black (extended)
color = 0
i += 1
repeat = (repeat << 8) + data[i]
elif code == 0xF: # White (extended)
color = 255
i += 1
repeat = (repeat << 8) + data[i]
else: # Grayscale (single byte)
color = (code << 4) | code
# Fill pixels
for _ in range(repeat):
pixels[pixel_pos] = color
pixel_pos += 1
i += 1
return pixels
```
#### Nibble-Coded RLE4 Encoder (PW0)
```python
def encode_rle4_nibble(pixels: bytes) -> bytes:
result = bytearray()
i = 0
while i < len(pixels):
color_nibble = pixels[i] >> 4
length = 1
# Count consecutive pixels
while (i + length < len(pixels) and
(pixels[i + length] >> 4) == color_nibble):
length += 1
# Encode run
if color_nibble in (0x0, 0xF): # Black or white
while length > 0:
run = min(length, 4095)
more = (run | (color_nibble << 12))
result.append(more >> 8)
result.append(more & 0xFF)
length -= run
else: # Grayscale
while length > 0:
run = min(length, 15)
result.append((run | (color_nibble << 4)))
length -= run
i += length
return result
```
#### CTB Variable RLE Decoder
```python
def decode_ctb_variable_rle(data: bytes, width: int, height: int) -> bytes:
pixels = bytearray(width * height)
pixel_pos = 0
i = 0
while i < len(data):
color_byte = data[i]
i += 1
grey7 = color_byte & 0x7F
color = 0 if grey7 == 0 else (grey7 << 1) | 1
if (color_byte & 0x80) == 0:
stride = 1
else:
length_byte = data[i]
i += 1
if (length_byte & 0x80) == 0:
stride = length_byte
elif (length_byte & 0x40) == 0:
stride = ((length_byte & 0x3F) << 8) | data[i]
i += 1
elif (length_byte & 0x20) == 0:
stride = ((length_byte & 0x1F) << 16) | (data[i] << 8) | data[i + 1]
i += 2
else:
stride = ((length_byte & 0x0F) << 24) | (data[i] << 16) | (data[i + 1] << 8) | data[i + 2]
i += 3
for _ in range(stride):
pixels[pixel_pos] = color
pixel_pos += 1
return pixels
```
### Dependencies by Format
**Minimal (No external libs):**
- Binary I/O, basic data structures
- Formats: CBDDLP, CTB, PWS, PW0, PhotonS, GOO, CXDLP, FDG
**Recommended:**
- ZIP library: SL1, UVJ, VDT, NanoDLP, ZCode, ZCodex, CWS, JXS
- JSON parser: UVJ, VDT, AnycubicZip, NanoDLP
- INI parser: SL1, JXS
- PNG encoder/decoder: All archive formats
**Advanced:**
- AES-256-CBC: CTBEncrypted
- XML parser: CWS, VDA, ZCode, SVGX
- SVG parser: SVGX
### Common Pitfalls
1. **Endianness:**
- Chitubox family: Little-endian (LE)
- PhotonS, GOO, CXDLP, MDLP: Big-endian (BE)
- **Always verify in format header!**
2. **Float Size:**
- **CTB v3 uses 32-bit floats, NOT 64-bit doubles**
- Previous docs had this wrong
- Verified from Chitubox 1.8 fixtures
3. **Nibble RLE Complexity:**
- PW0 nibble-coded RLE is variable-length
- NOT a fixed 3-byte format
- Must handle both 1-byte and 2-byte runs
4. **File Offsets:**
- Offsets are absolute from file start (position 0)
- Always seek before reading data sections
5. **RLE Limits:**
- RLE1/RLE125: max 125 pixels
- RLE128: max 128 pixels
- RLE4 nibble: max 4095 (B/W), 15 (grayscale)
- CTB variable RLE: variable length, up to 28-bit runs
- Split longer runs
6. **String Encoding:**
- Most: ASCII or UTF-8
- Anet: UTF-16 Big-Endian
- Always null-terminate and pad
### UVtools Source Cross-References
**CTB:**
- `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer`
- `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling
- `src/kiri/mode/sla/work/x_ctb_crypto.js`: native JS AES-256-CBC/no-padding, CTB layer XOR, encrypted CTB constants
**Anycubic:**
- `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel`
- `PrusaSlicer/printer/Anycubic Photon Mono 4.ini`: declares `FILEFORMAT_PM4N` without an explicit file version
---
## Format Comparison Quick Reference
### By Use Case
| Use Case | Format | Reason |
|----------|--------|--------|
| Maximum compatibility | CTB v3 | Widest printer support |
| Smallest file size | CBDDLP | Simple binary RLE |
| Best compression (AA) | PW0 (nibble RLE4) | Optimized variable-length |
| Archival/preservation | UVJ or SL1 | Open, PNG-based |
| Editing/manipulation | SL1 or UVJ | Human-readable config |
| Fastest decode | SL1, UVJ | Standard PNG decoders |
| Embedded systems | CBDDLP | Minimal complexity |
| Modern Anycubic | .pm5s (v518) | Latest features |
### By Complexity
| Level | Formats |
|-------|---------|
| **Low** | ImageFile, UVJ, SL1, CBDDLP, CTB v3, PWS |
| **Medium** | PW0, PhotonS, GOO, PHZ, FDG, CXDLP |
| **High** | AnycubicFile v517+, VDT, NanoDLP, ZCode |
| **Very High** | CTBEncrypted, SVGX, OSF |
---
## Corrections Summary
**v2 Changes from v1:**
1. **CTB v3 Header:** Fixed to 32-bit floats (was incorrectly documented as 64-bit doubles)
2. **Anycubic Split:** Separated legacy (PWS/PW0) from modern (.pm3n, .pm4n, .pm5s) formats
3. **RLE4 Encoding:** Fully documented nibble-coded variable-length algorithm (was simplified incorrectly)
4. **Version Support:** Added v517 (.pm3n, .pm5, .px6s) and v518 (.pm5s, .m5sp) details
5. **Capability Matrix:** Added comprehensive format capabilities table
6. **Extension Mapping:** Added machine detection by extension
7. **Preview Counts:** Documented v517 (7 previews) and v518 (9 previews) differences
8. **Implementation Order:** Revised based on actual complexity
---
## Validation Notes
**Verified Against:**
- UVtools.Core/FileFormats/ source code
- Chitubox 1.8 .ctb fixtures
- AnycubicFile.cs implementation (lines 2270-2409 for RLE4)
- ChituboxFile.cs Header class (lines 61-159)
- Format version constants and extension mappings
**Status:**
- ✓ CTB v3 header corrected (32-bit floats verified)
- ✓ Nibble-coded RLE4 algorithm extracted from working implementation
- ✓ Anycubic v517/v518 extensions documented
- ✓ All 32 formats have encode/decode capability
- ✓ 60+ file extensions cataloged
---
## License & Attribution
Derived from UVtools (https://github.com/sn4k3/UVtools)
License: AGPL-3.0
**Document Version:** 2.0
**Generated:** 2026-05-16