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

35 KiB

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:

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):

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):

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:

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:

[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:

{
  "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:

{
  "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

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)

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)

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)

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)

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

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