13 KiB
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
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:
1if stride > 1,0if stride == 1 - Length byte prefix bits indicate multi-byte length encoding
Detailed Encoding Logic (from source)
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
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):
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
-
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
-
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
- Quantize to 7-bit:
-
Encryption Handling:
- Always check
HeaderSettings.EncryptionKey - If non-zero, decrypt before RLE decode
- Use layer-specific key derivation
- Always check
Issue 2: .pm4n Version Classification - AMBIGUOUS
Current Status in UVtools
Extension Registration:
// AnycubicFile.cs:1058
new(typeof(AnycubicFile), "pm4n", "Photon Mono 4 (PM4N)")
Machine Mapping:
// AnycubicFile.cs:1731-1733
if (FileEndsWith(".pm4n"))
{
return AnyCubicMachine.PhotonMono4;
}
Version Mapping:
// 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
- Oversight: UVtools forgot to add .pm4n to the v517 case block
- Intentional: .pm4n can legitimately be multiple versions (firmware evolution)
- 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:
// 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:
- Obtain real .pm4n fixture files from PhotonMono4 printer
- Decode header and check
FileMarkSettings.Versionvalue - Verify layer table structure (92 bytes = v517, 96 bytes = v518)
- 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
- CTB RLE: Update main reference with variable-length encoding details
- .pm4n: Mark as "Assumed v517, requires fixture validation"
- Testing: Add note about lack of test fixtures in UVtools repository
- 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.