From 50e0a4674bdd50d6bb7cc58bc39b4502987ea543 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 16 May 2026 11:45:31 -0400 Subject: [PATCH] msla deltas and file format normalization --- docs/refs/MSLA_FORMAT_ADDENDUM.md | 859 +++++++------- docs/refs/MSLA_FORMAT_REFERENCE.md | 1700 +++++++++++++++------------- 2 files changed, 1316 insertions(+), 1243 deletions(-) diff --git a/docs/refs/MSLA_FORMAT_ADDENDUM.md b/docs/refs/MSLA_FORMAT_ADDENDUM.md index 786f9759..a8b39d82 100644 --- a/docs/refs/MSLA_FORMAT_ADDENDUM.md +++ b/docs/refs/MSLA_FORMAT_ADDENDUM.md @@ -1,429 +1,430 @@ -# mSLA Format Reference - Critical Addendum -## CTB RLE & PM4N Version Corrections - -**Date:** 2026-05-16 -**Status:** Code-verified corrections to v2 reference - ---- - -## Issue 1: CTB RLE Encoding - CORRECTED - -### Previous (Incorrect) Description -"RLE125: Simple 2-byte fixed format (color, length), max 125 pixels" - -### Actual Implementation (from ChituboxFile.cs:825-908) - -CTB uses a **7-bit grayscale variable-length RLE** with optional encryption, NOT simple RLE125. - -#### Encoding Algorithm - -**Step 1: Color Quantization** -```c -grey7 = pixel_value >> 1 // Convert 8-bit (0-255) to 7-bit (0-127) -``` - -**Step 2: Variable-Length Run Encoding** - -The encoding uses a **variable-length stride format**: - -| Stride Length | Bytes | Format | Description | -|--------------|-------|--------|-------------| -| 1 pixel | 1 byte | `[0ccccccc]` | Color only, bit 7 clear | -| 2-127 pixels | 2 bytes | `[1ccccccc] [0rrrrrrr]` | Color + 7-bit length | -| 128-16383 pixels | 3 bytes | `[1ccccccc] [10rrrrrr] [rrrrrrrr]` | Color + 14-bit length | -| 16384-2097151 pixels | 4 bytes | `[1ccccccc] [110rrrrr] [rrrrrrrr] [rrrrrrrr]` | Color + 21-bit length | -| 2097152-268435455 pixels | 5 bytes | `[1ccccccc] [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr]` | Color + 28-bit length | - -**Bit Layout:** -- `c` = color bits (7-bit grayscale, 0-127) -- `r` = repeat/stride bits -- Bit 7 of first byte: `1` if stride > 1, `0` if stride == 1 -- Length byte prefix bits indicate multi-byte length encoding - -#### Detailed Encoding Logic (from source) - -```c -void AddRep() -{ - if (stride == 0) return; - - if (stride > 1) { - color |= 0x80; // Set bit 7 to indicate run follows - } - rawData.Add(color); - - if (stride <= 1) { - // Single pixel: just the color byte - return; - } - - if (stride <= 0x7f) { // 127 - // 1-byte length: [0rrrrrrr] - rawData.Add((byte)stride); - return; - } - - if (stride <= 0x3fff) { // 16383 - // 2-byte length: [10rrrrrr] [rrrrrrrr] - rawData.Add((byte)((stride >> 8) | 0x80)); - rawData.Add((byte)stride); - return; - } - - if (stride <= 0x1fffff) { // 2097151 - // 3-byte length: [110rrrrr] [rrrrrrrr] [rrrrrrrr] - rawData.Add((byte)((stride >> 16) | 0xc0)); - rawData.Add((byte)(stride >> 8)); - rawData.Add((byte)stride); - return; - } - - if (stride <= 0xfffffff) { // 268435455 - // 4-byte length: [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr] - rawData.Add((byte)((stride >> 24) | 0xe0)); - rawData.Add((byte)(stride >> 16)); - rawData.Add((byte)(stride >> 8)); - rawData.Add((byte)stride); - } -} -``` - -#### Decoding Algorithm - -```python -def decode_ctb_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 - - # Extract 7-bit color - color = (color_byte & 0x7F) << 1 # Expand back to 8-bit - - # Check if run follows (bit 7 set) - if (color_byte & 0x80) == 0: - # Single pixel - pixels[pixel_pos] = color - pixel_pos += 1 - continue - - # Multi-pixel run: decode length - length_byte = data[i] - i += 1 - - if (length_byte & 0x80) == 0: - # 1-byte length (7 bits) - stride = length_byte - elif (length_byte & 0x40) == 0: - # 2-byte length (14 bits) - stride = ((length_byte & 0x3F) << 8) | data[i] - i += 1 - elif (length_byte & 0x20) == 0: - # 3-byte length (21 bits) - stride = ((length_byte & 0x1F) << 16) | (data[i] << 8) | data[i+1] - i += 2 - else: - # 4-byte length (28 bits) - stride = ((length_byte & 0x0F) << 24) | (data[i] << 16) | (data[i+1] << 8) | data[i+2] - i += 3 - - # Fill pixels - for _ in range(stride): - pixels[pixel_pos] = color - pixel_pos += 1 - - return pixels -``` - -#### Encryption (Optional) - -If `HeaderSettings.EncryptionKey > 0`, RLE data is XORed with a per-layer key: - -**Encryption Algorithm (from ChituboxFile.cs:2227-2248):** -```c -void LayerRleCryptBuffer(uint seed, uint layerIndex, byte[] input) -{ - if (seed == 0) return; - - uint init = seed * 0x2d83cdac + 0xd8a83423; - uint key = (layerIndex * 0x1e1530cd + 0xec3d47cd) * init; - - int index = 0; - for (int i = 0; i < input.Length; i++) - { - byte k = (byte)(key >> (8 * index)); - index++; - - if ((index & 3) == 0) { - key += init; - index = 0; - } - - input[i] = (byte)(input[i] ^ k); - } -} -``` - -**Key Generation:** -``` -init = seed * 0x2D83CDAC + 0xD8A83423 -key_base = (layerIndex * 0x1E1530CD + 0xEC3D47CD) * init - -For each 4-byte block: - XOR with bytes from key_base (little-endian) - After 4 bytes, update: key_base += init -``` - -#### Anti-Aliasing Support - -CTB format supports 7-bit grayscale (128 levels), enabling smooth anti-aliasing: -- 0x00 = fully masked (black) -- 0x7F = fully exposed (white) -- 0x01-0x7E = partial exposure (anti-aliasing) - -#### Example Encodings - -**Example 1: Single white pixel** -``` -Input: 1 pixel at value 255 -Encoded: 7F - - grey7 = 255 >> 1 = 127 = 0x7F - - stride = 1, so bit 7 stays clear - - Output: [0x7F] (1 byte) -``` - -**Example 2: Run of 100 white pixels** -``` -Input: 100 pixels at value 255 -Encoded: FF 64 - - grey7 = 127 = 0x7F - - stride = 100 = 0x64 - - 100 <= 127, so 1-byte length - - color with bit 7 set: 0x7F | 0x80 = 0xFF - - Output: [0xFF] [0x64] (2 bytes) -``` - -**Example 3: Run of 500 gray pixels (value 128)** -``` -Input: 500 pixels at value 128 -Encoded: C0 81 F4 - - grey7 = 128 >> 1 = 64 = 0x40 - - stride = 500 = 0x1F4 - - 500 > 127 and <= 16383, so 2-byte length - - color with bit 7: 0x40 | 0x80 = 0xC0 - - length high: (500 >> 8) | 0x80 = 0x01 | 0x80 = 0x81 - - length low: 500 & 0xFF = 0xF4 - - Output: [0xC0] [0x81] [0xF4] (3 bytes) -``` - -**Example 4: Run of 20000 pixels** -``` -Input: 20000 pixels at value 200 -Encoded: E4 C4 E2 20 - - grey7 = 200 >> 1 = 100 = 0x64 - - stride = 20000 = 0x4E20 - - 20000 > 16383, so 3-byte length - - color: 0x64 | 0x80 = 0xE4 - - length encoding: 0x4E20 with 0xC0 prefix - - byte 0: (0x4E20 >> 16) | 0xC0 = 0x00 | 0xC0 = 0xC0 - - Wait, let me recalculate... - - Actually 20000 = 0x4E20 - - High nibble: 0x4, middle: 0xE, low: 0x20 - - Prefix: ((0x4E20 >> 16) & 0x1F) | 0xC0 = 0x00 | 0xC0 = 0xC0 - - Byte 1: 0x4E20 >> 8 = 0x4E - - Byte 2: 0x4E20 & 0xFF = 0x20 - - Output: [0xE4] [0xC0] [0x4E] [0x20] (4 bytes) -``` - -#### Key Differences from Simple RLE125 - -| Feature | Simple RLE125 | CTB Variable RLE | -|---------|---------------|------------------| -| **Color depth** | 8-bit (0-255) | 7-bit (0-127) | -| **Max run (1-byte)** | 125 | 127 | -| **Max run (total)** | 125 | 268,435,455 | -| **Bytes per run** | 2 (fixed) | 1-5 (variable) | -| **Single pixel** | 2 bytes | 1 byte | -| **Anti-aliasing** | Binary or limited | Full 7-bit grayscale | -| **Encryption** | No | Optional per-layer XOR | -| **Compression** | Moderate | Better for long runs | - -#### Implementation Recommendations - -1. **Decoding CTB:** - - Read color byte - - Check bit 7 for run flag - - If clear: single pixel - - If set: decode variable-length stride - - Apply encryption if seed != 0 - -2. **Encoding CTB:** - - Quantize to 7-bit: `grey7 = pixel >> 1` - - Group consecutive identical pixels - - Encode with optimal byte count (1-5 bytes) - - Apply encryption if required - -3. **Encryption Handling:** - - Always check `HeaderSettings.EncryptionKey` - - If non-zero, decrypt before RLE decode - - Use layer-specific key derivation - ---- - -## Issue 2: .pm4n Version Classification - AMBIGUOUS - -### Current Status in UVtools - -**Extension Registration:** -```c -// AnycubicFile.cs:1058 -new(typeof(AnycubicFile), "pm4n", "Photon Mono 4 (PM4N)") -``` - -**Machine Mapping:** -```c -// AnycubicFile.cs:1731-1733 -if (FileEndsWith(".pm4n")) -{ - return AnyCubicMachine.PhotonMono4; -} -``` - -**Version Mapping:** -```c -// AnycubicFile.cs:1155-1191 -public override uint[] GetAvailableVersionsForExtension(string? extension) -{ - switch (extension) - { - case "pm3n": - case "pm5": - case "px6s": - return [VERSION_517]; // ← pm3n, pm5, px6s are v517 - - case "pm5s": - case "m5sp": - return [VERSION_518]; // ← pm5s, m5sp are v518 - - // NOTE: .pm4n is NOT in the switch statement! - - default: - return AvailableVersions; // ← .pm4n falls through here - } -} - -// AvailableVersions = [VERSION_1, VERSION_515, VERSION_516, VERSION_517, VERSION_518] -``` - -### The Ambiguity - -**`.pm4n` extension:** -- ✓ Registered as valid extension -- ✓ Maps to PhotonMono4 machine -- ✗ **NOT** explicitly assigned to any version in `GetAvailableVersionsForExtension` -- Returns all versions: `[1, 515, 516, 517, 518]` - -### Possible Interpretations - -1. **Oversight:** UVtools forgot to add .pm4n to the v517 case block -2. **Intentional:** .pm4n can legitimately be multiple versions (firmware evolution) -3. **Unknown:** Insufficient real-world fixtures to determine correct version - -### User Implementation Note - -**User's current implementation treats `.pm4n` as v517**, which is: -- ✓ Consistent with similar naming (.pm3n, .pm5 are v517) -- ✓ Logical based on product generation -- ⚠️ **UNVERIFIED** by UVtools code (could be any version) - -### Recommendations - -**For Implementation:** -```c -// Conservative approach (allow multiple versions): -if (extension == "pm4n") { - return [VERSION_515, VERSION_516, VERSION_517]; -} - -// Aggressive approach (assume v517 based on naming): -if (extension == "pm4n") { - return [VERSION_517]; -} -``` - -**For Validation:** -1. Obtain real .pm4n fixture files from PhotonMono4 printer -2. Decode header and check `FileMarkSettings.Version` value -3. Verify layer table structure (92 bytes = v517, 96 bytes = v518) -4. Document actual version found in fixtures - -**Fixture Verification Checklist:** -- [ ] Acquire .pm4n file from PhotonMono4 -- [ ] Read offset 0x02 (uint16 Version field) -- [ ] Check layer table entry size -- [ ] Verify preview count (2 = v515/516, 7 = v517, 9 = v518) -- [ ] Document findings - -### Comparison with Similar Extensions - -| Extension | Machine | Version in Code | Logic | -|-----------|---------|-----------------|-------| -| .pm3n | PhotonMono2 | 517 (explicit) | ✓ Defined | -| **.pm4n** | **PhotonMono4** | **??? (default all)** | ⚠️ **Ambiguous** | -| .pm5 | PhotonMonoM5 | 517 (explicit) | ✓ Defined | -| .pm5s | PhotonMonoM5s | 518 (explicit) | ✓ Defined | - -**Pattern suggests:** .pm4n should likely be v517, but this is **NOT verified in UVtools source**. - ---- - -## Updated Classification - -### Anycubic Modern Formats (CORRECTED) - -| Extension | Machine | Version | Status | -|-----------|---------|---------|--------| -| .pm3n | PhotonMono2 | 517 | ✓ Verified | -| **.pm4n** | **PhotonMono4** | **517 (assumed)** | **⚠️ UNVERIFIED** | -| .pm5 | PhotonMonoM5 | 517 | ✓ Verified | -| .px6s | PhotonMonoX6Ks | 517 | ✓ Verified | -| .pm5s | PhotonMonoM5s | 518 | ✓ Verified | -| .m5sp | PhotonMonoM5sPro | 518 | ✓ Verified | - -### Action Items for Documentation - -1. **CTB RLE:** Update main reference with variable-length encoding details -2. **.pm4n:** Mark as "Assumed v517, requires fixture validation" -3. **Testing:** Add note about lack of test fixtures in UVtools repository -4. **Encryption:** Document optional per-layer XOR encryption for CTB - ---- - -## Code References - -**CTB RLE Encoding:** -- Implementation: `ChituboxFile.cs:825-908` (EncodeCtbImage) -- Decoding: `ChituboxFile.cs:685-745` (DecodeImage) -- Encryption: `ChituboxFile.cs:2220-2248` (LayerRleCrypt) - -**Anycubic Versions:** -- Extension mapping: `AnycubicFile.cs:1155-1191` (GetAvailableVersionsForExtension) -- Machine detection: `AnycubicFile.cs:1731-1733` (FileEndsWith check) -- Version constants: `AnycubicFile.cs:30-33` (VERSION_515-518) - ---- - -## Summary - -**CTB RLE:** -- ❌ Previous: Simple RLE125 (2 bytes/run, max 125) -- ✅ Correct: Variable-length 7-bit grayscale RLE (1-5 bytes/run, max 268M, optional encryption) - -**.pm4n Version:** -- ❌ Previous: Listed under v517 without caveat -- ✅ Correct: Ambiguous in UVtools (defaults to all versions), **assumed v517 pending fixture validation** - -**Document Status:** Ready for integration into main reference with caveats noted. +# mSLA Format Reference - Critical Addendum +## CTB RLE & PM4N Version Corrections + +**Date:** 2026-05-16 +**Status:** Code-verified corrections to v2 reference + +--- + +## Issue 1: CTB RLE Encoding - CORRECTED + +### Previous (Incorrect) Description +"RLE125: Simple 2-byte fixed format (color, length), max 125 pixels" + +### Actual Implementation (from ChituboxFile.cs:825-908) + +CTB uses a **7-bit grayscale variable-length RLE** with optional encryption, NOT simple RLE125. + +#### Encoding Algorithm + +**Step 1: Color Quantization** +```c +grey7 = pixel_value >> 1 // Convert 8-bit (0-255) to 7-bit (0-127) +``` + +**Step 2: Variable-Length Run Encoding** + +The encoding uses a **variable-length stride format**: + +| Stride Length | Bytes | Format | Description | +|--------------|-------|--------|-------------| +| 1 pixel | 1 byte | `[0ccccccc]` | Color only, bit 7 clear | +| 2-127 pixels | 2 bytes | `[1ccccccc] [0rrrrrrr]` | Color + 7-bit length | +| 128-16383 pixels | 3 bytes | `[1ccccccc] [10rrrrrr] [rrrrrrrr]` | Color + 14-bit length | +| 16384-2097151 pixels | 4 bytes | `[1ccccccc] [110rrrrr] [rrrrrrrr] [rrrrrrrr]` | Color + 21-bit length | +| 2097152-268435455 pixels | 5 bytes | `[1ccccccc] [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr]` | Color + 28-bit length | + +**Bit Layout:** +- `c` = color bits (7-bit grayscale, 0-127) +- `r` = repeat/stride bits +- Bit 7 of first byte: `1` if stride > 1, `0` if stride == 1 +- Length byte prefix bits indicate multi-byte length encoding + +#### Detailed Encoding Logic (from source) + +```c +void AddRep() +{ + if (stride == 0) return; + + if (stride > 1) { + color |= 0x80; // Set bit 7 to indicate run follows + } + rawData.Add(color); + + if (stride <= 1) { + // Single pixel: just the color byte + return; + } + + if (stride <= 0x7f) { // 127 + // 1-byte length: [0rrrrrrr] + rawData.Add((byte)stride); + return; + } + + if (stride <= 0x3fff) { // 16383 + // 2-byte length: [10rrrrrr] [rrrrrrrr] + rawData.Add((byte)((stride >> 8) | 0x80)); + rawData.Add((byte)stride); + return; + } + + if (stride <= 0x1fffff) { // 2097151 + // 3-byte length: [110rrrrr] [rrrrrrrr] [rrrrrrrr] + rawData.Add((byte)((stride >> 16) | 0xc0)); + rawData.Add((byte)(stride >> 8)); + rawData.Add((byte)stride); + return; + } + + if (stride <= 0xfffffff) { // 268435455 + // 4-byte length: [1110rrrr] [rrrrrrrr] [rrrrrrrr] [rrrrrrrr] + rawData.Add((byte)((stride >> 24) | 0xe0)); + rawData.Add((byte)(stride >> 16)); + rawData.Add((byte)(stride >> 8)); + rawData.Add((byte)stride); + } +} +``` + +#### Decoding Algorithm + +```python +def decode_ctb_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 + + # Extract 7-bit color and expand back to 8-bit. + # Non-zero levels map to odd values so 0x7F becomes 0xFF. + grey7 = color_byte & 0x7F + color = 0 if grey7 == 0 else (grey7 << 1) | 1 + + # Check if run follows (bit 7 set) + if (color_byte & 0x80) == 0: + # Single pixel + pixels[pixel_pos] = color + pixel_pos += 1 + continue + + # Multi-pixel run: decode length + length_byte = data[i] + i += 1 + + if (length_byte & 0x80) == 0: + # 1-byte length (7 bits) + stride = length_byte + elif (length_byte & 0x40) == 0: + # 2-byte length (14 bits) + stride = ((length_byte & 0x3F) << 8) | data[i] + i += 1 + elif (length_byte & 0x20) == 0: + # 3-byte length (21 bits) + stride = ((length_byte & 0x1F) << 16) | (data[i] << 8) | data[i+1] + i += 2 + else: + # 4-byte length (28 bits) + stride = ((length_byte & 0x0F) << 24) | (data[i] << 16) | (data[i+1] << 8) | data[i+2] + i += 3 + + # Fill pixels + for _ in range(stride): + pixels[pixel_pos] = color + pixel_pos += 1 + + return pixels +``` + +#### Encryption (Optional) + +If the CTB header encryption seed is non-zero, RLE data is XORed with a per-layer key stream derived from that seed: + +**Encryption Algorithm (from ChituboxFile.cs:2227-2248):** +```c +void LayerRleCryptBuffer(uint seed, uint layerIndex, byte[] input) +{ + if (seed == 0) return; + + uint init = seed * 0x2d83cdac + 0xd8a83423; + uint key = (layerIndex * 0x1e1530cd + 0xec3d47cd) * init; + + int index = 0; + for (int i = 0; i < input.Length; i++) + { + byte k = (byte)(key >> (8 * index)); + index++; + + if ((index & 3) == 0) { + key += init; + index = 0; + } + + input[i] = (byte)(input[i] ^ k); + } +} +``` + +**Key Generation:** +``` +init = seed * 0x2D83CDAC + 0xD8A83423 +key_base = (layerIndex * 0x1E1530CD + 0xEC3D47CD) * init + +For each 4-byte block: + XOR with bytes from key_base (little-endian) + After 4 bytes, update: key_base += init +``` + +#### Anti-Aliasing Support + +CTB format supports 7-bit grayscale (128 levels), enabling smooth anti-aliasing: +- 0x00 = fully masked (black) +- 0x7F = fully exposed (white) +- 0x01-0x7E = partial exposure (anti-aliasing) + +#### Example Encodings + +**Example 1: Single white pixel** +``` +Input: 1 pixel at value 255 +Encoded: 7F + - grey7 = 255 >> 1 = 127 = 0x7F + - stride = 1, so bit 7 stays clear + - Output: [0x7F] (1 byte) +``` + +**Example 2: Run of 100 white pixels** +``` +Input: 100 pixels at value 255 +Encoded: FF 64 + - grey7 = 127 = 0x7F + - stride = 100 = 0x64 + - 100 <= 127, so 1-byte length + - color with bit 7 set: 0x7F | 0x80 = 0xFF + - Output: [0xFF] [0x64] (2 bytes) +``` + +**Example 3: Run of 500 gray pixels (value 128)** +``` +Input: 500 pixels at value 128 +Encoded: C0 81 F4 + - grey7 = 128 >> 1 = 64 = 0x40 + - stride = 500 = 0x1F4 + - 500 > 127 and <= 16383, so 2-byte length + - color with bit 7: 0x40 | 0x80 = 0xC0 + - length high: (500 >> 8) | 0x80 = 0x01 | 0x80 = 0x81 + - length low: 500 & 0xFF = 0xF4 + - Output: [0xC0] [0x81] [0xF4] (3 bytes) +``` + +**Example 4: Run of 20000 pixels** +``` +Input: 20000 pixels at value 200 +Encoded: E4 C0 4E 20 + - grey7 = 200 >> 1 = 100 = 0x64 + - stride = 20000 = 0x4E20 + - 20000 > 16383, so 3-byte length + - color: 0x64 | 0x80 = 0xE4 + - length encoding: 0x4E20 with 0xC0 prefix + - Byte 0: ((0x4E20 >> 16) & 0x1F) | 0xC0 = 0xC0 + - Byte 1: (0x4E20 >> 8) & 0xFF = 0x4E + - Byte 2: 0x4E20 & 0xFF = 0x20 + - Output: [0xE4] [0xC0] [0x4E] [0x20] (4 bytes) +``` + +#### Key Differences from Simple RLE125 + +| Feature | Simple RLE125 | CTB Variable RLE | +|---------|---------------|------------------| +| **Color depth** | 8-bit (0-255) | 7-bit (0-127) | +| **Max run (1-byte)** | 125 | 127 | +| **Max run (total)** | 125 | 268,435,455 | +| **Bytes per run** | 2 (fixed) | 1-5 (variable) | +| **Single pixel** | 2 bytes | 1 byte | +| **Anti-aliasing** | Binary or limited | Full 7-bit grayscale | +| **Encryption** | No | Optional per-layer XOR | +| **Compression** | Moderate | Better for long runs | + +#### Implementation Recommendations + +1. **Decoding CTB:** + - Read color byte + - Check bit 7 for run flag + - If clear: single pixel + - If set: decode variable-length stride + - Apply encryption if seed != 0 + +2. **Encoding CTB:** + - Quantize to 7-bit: `grey7 = pixel >> 1` + - Group consecutive identical pixels + - Encode with optimal byte count (1-5 bytes) + - Apply encryption if required + +3. **Encryption Handling:** + - Always check the CTB header encryption seed + - If non-zero, decrypt before RLE decode + - Use layer-specific key derivation + +--- + +## Issue 2: .pm4n Version Classification - AMBIGUOUS + +### Current Status in UVtools + +**Extension Registration:** +```c +// AnycubicFile.cs:1058 +new(typeof(AnycubicFile), "pm4n", "Photon Mono 4 (PM4N)") +``` + +**Machine Mapping:** +```c +// AnycubicFile.cs:1731-1733 +if (FileEndsWith(".pm4n")) +{ + return AnyCubicMachine.PhotonMono4; +} +``` + +**Version Mapping:** +```c +// AnycubicFile.cs:1155-1191 +public override uint[] GetAvailableVersionsForExtension(string? extension) +{ + switch (extension) + { + case "pm3n": + case "pm5": + case "px6s": + return [VERSION_517]; // ← pm3n, pm5, px6s are v517 + + case "pm5s": + case "m5sp": + return [VERSION_518]; // ← pm5s, m5sp are v518 + + // NOTE: .pm4n is NOT in the switch statement! + + default: + return AvailableVersions; // ← .pm4n falls through here + } +} + +// AvailableVersions = [VERSION_1, VERSION_515, VERSION_516, VERSION_517, VERSION_518] +``` + +### The Ambiguity + +**`.pm4n` extension:** +- ✓ Registered as valid extension +- ✓ Maps to PhotonMono4 machine +- ✗ **NOT** explicitly assigned to any version in `GetAvailableVersionsForExtension` +- Returns all versions: `[1, 515, 516, 517, 518]` + +### Possible Interpretations + +1. **Oversight:** UVtools forgot to add .pm4n to the v517 case block +2. **Intentional:** .pm4n can legitimately be multiple versions (firmware evolution) +3. **Unknown:** Insufficient real-world fixtures to determine correct version + +### User Implementation Note + +**User's current implementation treats `.pm4n` as v517**, which is: +- ✓ Consistent with similar naming (.pm3n, .pm5 are v517) +- ✓ Logical based on product generation +- ⚠️ **UNVERIFIED** by UVtools code (could be any version) + +### Recommendations + +**For Implementation:** +```c +// Conservative approach (allow multiple versions): +if (extension == "pm4n") { + return [VERSION_515, VERSION_516, VERSION_517]; +} + +// Aggressive approach (assume v517 based on naming): +if (extension == "pm4n") { + return [VERSION_517]; +} +``` + +**For Validation:** +1. Obtain real .pm4n fixture files from PhotonMono4 printer +2. Decode header and check `FileMarkSettings.Version` value +3. Verify header table length (92 bytes = v517, 96 bytes = v518) +4. Verify layer definition entries remain 32 bytes each; v518 may also include `SUBIMGS` sublayer entries +5. Document actual version found in fixtures + +**Fixture Verification Checklist:** +- [ ] Acquire .pm4n file from PhotonMono4 +- [ ] Verify file mark starts with ASCII `ANYCUBIC` +- [ ] Read version at offset 0x0C (little-endian uint32 in the `FileMark`) +- [ ] Check `HEADER` table length (92 = v517, 96 = v518) +- [ ] Check layer definition entries are 32 bytes each +- [ ] Check whether `SUBIMGS` and `PREVIEW2` tables are present +- [ ] Document findings + +### Comparison with Similar Extensions + +| Extension | Machine | Version in Code | Logic | +|-----------|---------|-----------------|-------| +| .pm3n | PhotonMono2 | 517 (explicit) | ✓ Defined | +| **.pm4n** | **PhotonMono4** | **??? (default all)** | ⚠️ **Ambiguous** | +| .pm5 | PhotonMonoM5 | 517 (explicit) | ✓ Defined | +| .pm5s | PhotonMonoM5s | 518 (explicit) | ✓ Defined | + +**Pattern suggests:** .pm4n should likely be v517, but this is **NOT verified in UVtools source**. + +--- + +## Updated Classification + +### Anycubic Modern Formats (CORRECTED) + +| Extension | Machine | Version | Status | +|-----------|---------|---------|--------| +| .pm3n | PhotonMono2 | 517 | ✓ Verified | +| **.pm4n** | **PhotonMono4** | **517 (assumed)** | **⚠️ UNVERIFIED** | +| .pm5 | PhotonMonoM5 | 517 | ✓ Verified | +| .px6s | PhotonMonoX6Ks | 517 | ✓ Verified | +| .pm5s | PhotonMonoM5s | 518 | ✓ Verified | +| .m5sp | PhotonMonoM5sPro | 518 | ✓ Verified | + +### Action Items for Documentation + +1. **CTB RLE:** Update main reference with variable-length encoding details +2. **.pm4n:** Mark as "Assumed v517, requires fixture validation" +3. **Testing:** Add note about lack of test fixtures in UVtools repository +4. **Encryption:** Document optional per-layer XOR encryption for CTB + +--- + +## Code References + +**CTB RLE Encoding:** +- Implementation: `ChituboxFile.cs:825-908` (EncodeCtbImage) +- Decoding: `ChituboxFile.cs:685-745` (DecodeImage) +- Encryption: `ChituboxFile.cs:2220-2248` (LayerRleCrypt) + +**Anycubic Versions:** +- Extension mapping: `AnycubicFile.cs:1155-1191` (GetAvailableVersionsForExtension) +- Machine detection: `AnycubicFile.cs:1731-1733` (FileEndsWith check) +- Version constants: `AnycubicFile.cs:30-33` (VERSION_515-518) + +--- + +## Summary + +**CTB RLE:** +- ❌ Previous: Simple RLE125 (2 bytes/run, max 125) +- ✅ Correct: Variable-length 7-bit grayscale RLE (1-5 bytes/run, max 268M, optional encryption) + +**.pm4n Version:** +- ❌ Previous: Listed under v517 without caveat +- ✅ Correct: Ambiguous in UVtools (defaults to all versions), **assumed v517 pending fixture validation** + +**Document Status:** Ready for integration into main reference with caveats noted. diff --git a/docs/refs/MSLA_FORMAT_REFERENCE.md b/docs/refs/MSLA_FORMAT_REFERENCE.md index c5dc9730..87ea8319 100644 --- a/docs/refs/MSLA_FORMAT_REFERENCE.md +++ b/docs/refs/MSLA_FORMAT_REFERENCE.md @@ -1,814 +1,886 @@ -# mSLA File Format Reference - -**Generated:** 2026-05-16 -**Total Formats:** 32 - ---- - -## 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 | Most common | -| CTBEncryptedFile | ✓ | ✓ | ctb, encrypted.ctb | 4-5 (def: 5) | No | RLE125+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, pwx | 1 | Yes (22) | RLE1 (125) | PhotonS, PhotonX | -| AnycubicFile (PW0) | ✓ | ✓ | pw0 | 1 | Yes (22) | Nibble RLE4 | PhotonZero | -| **Anycubic Family - Modern (v515-518)** | -| AnycubicFile (v515) | ✓ | ✓ | pwmx, pwmo, dl2p, pwmb, pmx2 | 515-517 | Yes (22) | Nibble RLE4 | Mono series | -| 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 (all) | ✓ | ✓ | dlp, pwms, pwma, pmsq, pm3, pm3m, pm3r, pwc | varies | Yes (22) | Nibble RLE4 | Additional models | -| AnycubicZipFile | ✓ | ✓ | pm4u, pm7, pm7m, pwsz, pp1, pp1m | - | No | PNG | ZIP with JSON | -| **Creality Family** | -| CrealityCXDLPv1File | ✓ | ✓ | v1.cxdlp | 1 | No | RLE | Legacy | -| 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 | RLE+delim | 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:** RLE125 (limit: 125 pixels) - -**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 | Padding | -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. - -#### 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:** RLE125 - -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 - -### 2. Anycubic Family - -#### 2.1 Legacy Anycubic Formats (v1) - -##### 2.1.1 PWS (Photon / Photon S) - -**Extensions:** `.pws`, `.pwx` -**Version:** 1 -**RLE:** RLE1 (limit: 125) -**Machines:** PhotonS, PhotonX - -**RLE1 Encoding (PWS):** -``` -For each run (2 bytes): - byte[0] = color (0x00 = black, 0xFF = white) - byte[1] = length (1-125) - -Constant: RLE1EncodingLimit = 0x7D (125) -``` - -##### 2.1.2 PW0 (Photon Zero) - CORRECTED - -**Extensions:** `.pw0` -**Version:** 1 -**RLE:** Nibble-coded RLE4 (limit: 4095) -**Machine:** PhotonZero - -**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 515-516 Formats - -**Extensions:** `.pwmx`, `.pwmo`, `.dl2p`, `.pwmb`, `.pmx2`, `.dlp`, `.pwms`, `.pwma`, `.pmsq`, `.pm3`, `.pm3m`, `.pm3r` - -**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` - -**Machines:** -- PhotonMono2 (.pm3n) -- PhotonMonoM5 (.pm5) -- PhotonMonoX6Ks (.px6s) - -**New Features:** -- 7 preview images (vs 2 in v516) -- Preview sizes: 224x168, 330x190 -- Layer table entry size: 92 bytes - -##### 2.2.3 Version 518 Formats - -**Extensions:** `.pm5s`, `.m5sp`, `.pwc` - -**Machines:** -- PhotonMonoM5s (.pm5s) -- PhotonMonoM5sPro (.m5sp) -- AnycubicCustomMachine (.pwc) - -**New Features:** -- 9 preview images -- 11 distinct printer profiles -- Preview sizes: 224x168, 330x190 -- Layer table entry size: 96 bytes -- 15 slicer metadata fields - -**Anycubic File Header (v515+):** -``` -Offset | Size | Type | Field | Notes --------|------|--------|--------------------------|------------------ -0x00 | 2 | uint16 | HeaderSize | bytes -0x02 | 2 | uint16 | Version | 515-518 -0x04 | 4 | float | BedSizeX | mm -0x08 | 4 | float | BedSizeY | mm -0x0C | 4 | float | BedSizeZ | mm -... -(remainder similar to v1, with version-specific extensions) -``` - -**Machine Detection (Extension → Machine mapping):** -``` -.pm3n → PhotonMono2 -.pm4n → PhotonMono4 -.pm5 → PhotonMonoM5 -.pm5s → PhotonMonoM5s -.m5sp → PhotonMonoM5sPro -.px6s → PhotonMonoX6Ks -.pwms → PhotonMonoSE -.pwma → PhotonMono4K -.pwmx → PhotonMonoX -.pwmo → PhotonMono -.pwmb → PhotonMonoX6K / PhotonM3Plus -``` - -#### 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 (PWS) | Run-Length 1 | 2 (fixed) | 125 | 8 | Color, Length | -| RLE4 (PW0) | Nibble-coded | 1-2 (variable) | 4095 (B/W), 15 (gray) | 8 | Optimized variable-length | -| RLE125 (CTB) | Run-Length 125 | 2 (fixed) | 125 | 8 | Color, Length | -| RLE128 (PhotonS) | Run-Length 128 | 2 (fixed) | 128 | 8 | Length, Color (BE) | -| RLE+Delim (GOO) | Delimited | 5 (+ delim) | 65535 | 8 | 0x55 + Color + Length16 + 0x0D0A | - -**Performance Comparison:** -- **RLE1/RLE125/RLE128:** Simple, predictable, easy to implement -- **Nibble RLE4:** Best compression for grayscale AA, complex encoding -- **RLE+Delim:** Highest max repeat, overhead from delimiters - ---- - -## 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:** Delimiter-based - -**RLE Format:** -``` -Each run (5 bytes): - 0x55 Magic byte - [color] 0x00 or 0xFF - [length_high] Big-endian uint16 - [length_low] - 0x0D 0x0A CRLF delimiter - -Maximum repeat: 65535 pixels -``` - -### 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}, - ... - ] -} -``` - ---- - -## 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** - Delimiter-based RLE - -**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 Decoder (CBDDLP, CTB) -```python -def decode_rle125(data: bytes, width: int, height: int) -> bytes: - pixels = bytearray(width * height) - pixel_index = 0 - data_index = 0 - - while data_index < len(data): - color = data[data_index] - length = data[data_index + 1] - data_index += 2 - - for _ in range(length): - pixels[pixel_index] = color - pixel_index += 1 - - return pixels -``` - -#### RLE125 Encoder -```python -def encode_rle125(pixels: bytes) -> bytes: - result = bytearray() - i = 0 - - while i < len(pixels): - color = pixels[i] - length = 1 - - while (i + length < len(pixels) and - pixels[i + length] == color and - length < 125): - length += 1 - - result.append(color) - result.append(length) - i += length - - 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 -``` - -### 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) - - Split longer runs - -6. **String Encoding:** - - Most: ASCII or UTF-8 - - Anet: UTF-16 Big-Endian - - Always null-terminate and pad - ---- - -## 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 - +# mSLA File Format Reference + +**Generated:** 2026-05-16 +**Total Formats:** 32 + +--- + +## 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, pwx | 1 | Yes (22) | RLE1 (125) | PhotonS, PhotonX | +| AnycubicFile (PW0) | ✓ | ✓ | pw0 | 1 | Yes (22) | Nibble RLE4 | PhotonZero | +| **Anycubic Family - Modern (v515-518)** | +| AnycubicFile (v515) | ✓ | ✓ | pwmx, pwmo, dl2p, pwmb, pmx2 | 515-517 | Yes (22) | Nibble RLE4 | Mono series | +| 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 (all) | ✓ | ✓ | dlp, pwms, pwma, pmsq, pm3, pm3m, pm3r, pwc | varies | Yes (22) | Nibble RLE4 | Additional models | +| AnycubicZipFile | ✓ | ✓ | pm4u, pm7, pm7m, pwsz, pp1, pp1m | - | No | PNG | ZIP with JSON | +| **Creality Family** | +| CrealityCXDLPv1File | ✓ | ✓ | v1.cxdlp | 1 | No | RLE | Legacy | +| 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 | RLE+delim | 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. + +#### 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 + +### 2. Anycubic Family + +#### 2.1 Legacy Anycubic Formats (v1) + +##### 2.1.1 PWS (Photon / Photon S) + +**Extensions:** `.pws`, `.pwx` +**Version:** 1 +**RLE:** RLE1 (limit: 125) +**Machines:** PhotonS, PhotonX + +**RLE1 Encoding (PWS):** +``` +For each run (2 bytes): + byte[0] = color (0x00 = black, 0xFF = white) + byte[1] = length (1-125) + +Constant: RLE1EncodingLimit = 0x7D (125) +``` + +##### 2.1.2 PW0 (Photon Zero) - CORRECTED + +**Extensions:** `.pw0` +**Version:** 1 +**RLE:** Nibble-coded RLE4 (limit: 4095) +**Machine:** PhotonZero + +**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 515-516 Formats + +**Extensions:** `.pwmx`, `.pwmo`, `.dl2p`, `.pwmb`, `.pmx2`, `.dlp`, `.pwms`, `.pwma`, `.pmsq`, `.pm3`, `.pm3m`, `.pm3r` + +**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) + +**New Features:** +- Additional preview/model/software metadata compared with earlier versions +- Preview sizes: 224x168, 330x190 +- `HEADER` table length: 92 bytes +- `LayerDef` entries remain 32 bytes each + +##### 2.2.3 Version 518 Formats + +**Extensions:** `.pm5s`, `.m5sp`, `.pwc` + +**Machines:** +- PhotonMonoM5s (.pm5s) +- PhotonMonoM5sPro (.m5sp) +- AnycubicCustomMachine (.pwc) + +**New Features:** +- 9 preview images +- 11 distinct printer profiles +- Preview sizes: 224x168, 330x190 +- `HEADER` table length: 96 bytes +- `LayerDef` entries remain 32 bytes each; `SUBIMGS` sublayer records may also be present +- 15 slicer metadata fields + +**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 +``` + +#### 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 (PWS) | Run-Length 1 | 2 (fixed) | 125 | 8 | Color, Length | +| 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) | +| RLE+Delim (GOO) | Delimited | 5 (+ delim) | 65535 | 8 | 0x55 + Color + Length16 + 0x0D0A | + +**Performance Comparison:** +- **RLE1/RLE125/RLE128:** Simple, predictable, easy to implement +- **CTB variable RLE:** Better compression and grayscale AA, plus optional seed-derived XOR +- **Nibble RLE4:** Best compression for grayscale AA, complex encoding +- **RLE+Delim:** Highest max repeat, overhead from delimiters + +--- + +## 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:** Delimiter-based + +**RLE Format:** +``` +Each run (5 bytes): + 0x55 Magic byte + [color] 0x00 or 0xFF + [length_high] Big-endian uint16 + [length_low] + 0x0D 0x0A CRLF delimiter + +Maximum repeat: 65535 pixels +``` + +### 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}, + ... + ] +} +``` + +--- + +## 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** - Delimiter-based RLE + +**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 Decoder (CBDDLP) +```python +def decode_rle125(data: bytes, width: int, height: int) -> bytes: + pixels = bytearray(width * height) + pixel_index = 0 + data_index = 0 + + while data_index < len(data): + color = data[data_index] + length = data[data_index + 1] + data_index += 2 + + for _ in range(length): + pixels[pixel_index] = color + pixel_index += 1 + + return pixels +``` + +#### RLE125 Encoder (CBDDLP) +```python +def encode_rle125(pixels: bytes) -> bytes: + result = bytearray() + i = 0 + + while i < len(pixels): + color = pixels[i] + length = 1 + + while (i + length < len(pixels) and + pixels[i + length] == color and + length < 125): + length += 1 + + result.append(color) + result.append(length) + i += length + + 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 + +--- + +## 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 +