# 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