adeed manual processing using UVtools

This commit is contained in:
cpu
2026-06-12 13:36:22 +02:00
parent a731b064b6
commit 9a8635aa98
11 changed files with 340 additions and 38 deletions

BIN
diagnose/Dummy.FCStd Normal file

Binary file not shown.

BIN
diagnose/Dummy.stl Normal file

Binary file not shown.

71
diagnose/decode_prev.py Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Decode the PREV (preview thumbnail) section from a pm4n file and save as PNG.
Run: python3 decode_prev.py <file.pm4n>
"""
import struct, sys
from pathlib import Path
path = sys.argv[1]
data = open(path, 'rb').read()
prev_off = data.find(b'PREV')
print(f"PREV at 0x{prev_off:06X}")
# PREV section layout (from ANYCUBIC format docs and common reverse-engineering):
# +0x00 "PREV"
# +0x04 u32 section_length
# +0x08 u32 image_width
# +0x0C u32 image_height
# +0x10 u32 image_data_length
# +0x14 image data (RGB565 or RLE)
for hdr_size in [0x14, 0x18, 0x10, 0x0C]:
w = struct.unpack_from('<I', data, prev_off + 0x08)[0]
h = struct.unpack_from('<I', data, prev_off + 0x0C)[0]
dlen = struct.unpack_from('<I', data, prev_off + 0x10)[0]
print(f" PREV+0x08: w={w}, h={h}, dlen={dlen}")
if 100 < w < 2000 and 100 < h < 2000:
print(f" -> plausible dimensions {w}x{h}, data at +0x14, {dlen} bytes")
break
# Show first 64 bytes of PREV section
print(f"\nPREV raw (first 64 bytes):")
chunk = data[prev_off:prev_off+64]
for i in range(0, 64, 16):
row = chunk[i:i+16]
hex_p = ' '.join(f'{b:02X}' for b in row)
u_vals = [struct.unpack_from('<I', row, j)[0] for j in range(0, min(16,len(row))-3, 4)]
print(f" +0x{i:02X} {hex_p:<48} {u_vals}")
# Try to decode as RGB565 (common for ANYCUBIC previews)
# Each pixel = 2 bytes, little-endian RGB565
# R = bits[15:11], G = bits[10:5], B = bits[4:0]
img_data_off = prev_off + 0x14
img_data_len = struct.unpack_from('<I', data, prev_off + 0x10)[0]
w = struct.unpack_from('<I', data, prev_off + 0x08)[0]
h = struct.unpack_from('<I', data, prev_off + 0x0C)[0]
print(f"\nAttempting RGB565 decode: {w}x{h}, {img_data_len} bytes at 0x{img_data_off:06X}")
if 0 < w < 2000 and 0 < h < 2000 and img_data_len == w * h * 2:
print(f" Size matches RGB565 ({w}*{h}*2={w*h*2}) ✓")
try:
from PIL import Image
pixels = []
raw = data[img_data_off:img_data_off + img_data_len]
for i in range(0, len(raw)-1, 2):
px = raw[i] | (raw[i+1] << 8)
r = ((px >> 11) & 0x1F) << 3
g = ((px >> 5) & 0x3F) << 2
b = (px & 0x1F) << 3
pixels.append((r, g, b))
img = Image.new('RGB', (w, h))
img.putdata(pixels)
out = Path(path).with_suffix('.prev_thumb.png')
img.save(out)
print(f" Saved thumbnail to {out}")
except Exception as e:
print(f" Error: {e}")
else:
print(f" Size mismatch or bad dims, skipping decode")
print(f" Expected {w*h*2} bytes for RGB565, got {img_data_len}")

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Decode the composite block and first layer block from a pm4n file,
report pixel counts and first/last pixel values.
Run: python3 decode_rle_blocks.py output/pm4n/Flow_Controller_Panel-Front.pm4n
"""
import struct, sys
path = sys.argv[1]
data = open(path, 'rb').read()
fsize = len(data)
print(f"File: {path} ({fsize} bytes)\n")
LAYE_HDR = 0x20
STRIDE = 0x20
laye = data.find(b'LAYE')
n_entries = struct.unpack_from('<I', data, laye + 0x10)[0]
composite_off = struct.unpack_from('<I', data, laye + 0x14)[0]
block_size = struct.unpack_from('<I', data, laye + 0x18)[0]
print(f"LAYE n_entries={n_entries} composite_off=0x{composite_off:06X} block_size={block_size}")
def decode_rle(data, offset, max_bytes):
"""Decode RLE starting at offset, stop after max_bytes read. Returns pixel list."""
pixels = []
i = offset
end = min(offset + max_bytes, len(data))
while i + 1 < end:
b0 = data[i]; b1 = data[i+1]
nibble = (b0 >> 4) & 0xF
colour = 255 if nibble == 0xF else 0
run = ((b0 & 0x0F) << 8) | b1
run += 1
pixels.extend([colour] * run)
i += 2
return pixels, i - offset
def summarize_block(name, offset, size):
print(f"\n{name} at 0x{offset:06X}, {size} bytes:")
if offset + size > fsize:
print(f" *** PAST EOF (file ends at 0x{fsize:06X}) ***")
size = max(0, fsize - offset)
# Show first 16 bytes raw
raw = data[offset:offset+16]
print(f" First 16 bytes: {raw.hex()}")
# Decode RLE
pixels, bytes_consumed = decode_rle(data, offset, size)
print(f" Decoded: {len(pixels)} pixels from {bytes_consumed} bytes")
print(f" Expected: {9024*5120} pixels ({9024}×{5120})")
if pixels:
whites = sum(1 for p in pixels if p >= 128)
blacks = len(pixels) - whites
print(f" White px: {whites} ({100*whites/len(pixels):.1f}%)")
print(f" Black px: {blacks} ({100*blacks/len(pixels):.1f}%)")
# Check if it decodes to correct pixel count
if len(pixels) == 9024 * 5120:
print(f" ✓ Correct pixel count")
else:
print(f" *** WRONG pixel count (off by {len(pixels) - 9024*5120})")
# Show first few runs
print(f" First pixels: {pixels[:20]}")
# Composite block
summarize_block("Composite block (block 0)", composite_off, block_size)
# First entry block
e0_base = laye + LAYE_HDR
e0_data_off = struct.unpack_from('<I', data, e0_base + 0x14)[0]
e0_data_sz = struct.unpack_from('<I', data, e0_base + 0x18)[0]
summarize_block("Entry[0] block", e0_data_off, e0_data_sz)

107
diagnose/diagnose_pm4n.py Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Run this on your machine: python3 diagnose_pm4n.py Dummy.pm4n"""
import struct, sys
path = sys.argv[1] if len(sys.argv) > 1 else 'Dummy.pm4n'
data = open(path, 'rb').read()
fsize = len(data)
print(f"File: {path} ({fsize} bytes)")
print()
# Parse ANYCUBIC header
magic = data[0:8]
version = struct.unpack_from('<I', data, 8)[0]
n_sections = struct.unpack_from('<I', data, 12)[0]
header_size = struct.unpack_from('<I', data, 16)[0]
print(f"Magic: {magic}")
print(f"Bytes 8-11: {data[8:12].hex()} (={version})")
print(f"Section count: {n_sections}")
print(f"Header size: {header_size} (0x{header_size:X})")
print()
# Section table: n_sections entries of (offset:u32, length:u32) starting at byte 20
print(f"Section table ({n_sections} entries from offset 20):")
sections = []
for i in range(n_sections):
base = 20 + i * 8
if base + 8 > fsize:
break
off = struct.unpack_from('<I', data, base)[0]
ln = struct.unpack_from('<I', data, base+4)[0]
sections.append((off, ln))
for idx, (off, ln) in enumerate(sections):
tag = data[off:off+4] if off + 4 <= fsize else b'????'
tag_str = tag.decode('ascii', errors='replace')
print(f" [{idx:2d}] offset=0x{off:06X} ({off:7d}) length={ln:7d} tag@offset={tag_str!r}")
print()
# Also peek at each section start for tag-like content
print("Content at each section offset (first 32 bytes):")
for idx, (off, ln) in enumerate(sections):
if off + 16 <= fsize:
chunk = data[off:off+32]
# Try to find sub-tags
for sub_off in range(0, min(32, len(chunk))-3):
sub_tag = chunk[sub_off:sub_off+4]
if all(32 <= b < 127 for b in sub_tag):
sub_len = struct.unpack_from('<I', chunk, sub_off+4)[0] if sub_off+8 <= len(chunk) else 0
print(f" section[{idx}]+0x{sub_off:02X} tag={sub_tag.decode()!r} next_u32={sub_len}")
break
else:
print(f" section[{idx}] hex: {chunk[:16].hex()}")
print()
# Look for exposure-like floats (1.0 to 600.0) across the whole file
print("Float values in range [1.0 .. 600.0] across whole file:")
for off in range(0, fsize - 3, 4):
v = struct.unpack_from('<f', data, off)[0]
if 1.0 <= v <= 600.0 and v == round(v, 1):
# Show context
section_hint = next((f"sec[{i}]+{off-s:d}" for i,(s,l) in enumerate(sections) if s <= off < s+l), "outside")
print(f" 0x{off:06X} {v:.1f} ({section_hint})")
# ---------------------------------------------------------------------------
# Extra: dump LAYE entries with CORRECT field layout
# (verified from binary probe: HDR=0x20, exposure@+0x00, data_off@+0x14, data_size@+0x18)
# ---------------------------------------------------------------------------
def find_tag2(d, tag):
i = d.find(tag)
return i if i >= 0 else None
LAYE_HDR_SIZE2 = 0x20
ENTRY_STRIDE2 = 0x20
laye2 = find_tag2(data, b'LAYE')
if laye2 is not None:
n_entries2 = struct.unpack_from('<I', data, laye2 + 0x10)[0]
composite_off2 = struct.unpack_from('<I', data, laye2 + 0x14)[0]
block_size2 = struct.unpack_from('<I', data, laye2 + 0x18)[0]
print(f"\nLAYE (correct layout) at 0x{laye2:06X}:")
print(f" n_entries = {n_entries2}")
print(f" composite_off= 0x{composite_off2:06X}")
print(f" block_size = {block_size2}")
expected_off = composite_off2
ok = True
for i in range(n_entries2):
base = laye2 + LAYE_HDR_SIZE2 + i * ENTRY_STRIDE2
if base + ENTRY_STRIDE2 > fsize:
break
exp = struct.unpack_from('<f', data, base + 0x00)[0]
z_pos = struct.unpack_from('<f', data, base + 0x04)[0]
d_off = struct.unpack_from('<I', data, base + 0x14)[0]
d_sz = struct.unpack_from('<I', data, base + 0x18)[0]
expected_off += block_size2
match = "OK" if d_off == expected_off and d_sz == block_size2 else "*** MISMATCH ***"
if match != "OK":
ok = False
print(f" entry[{i:2d}]: exp={exp:.1f}s z={z_pos:.2f}mm "
f"data_off=0x{d_off:06X} (exp 0x{expected_off:06X}) "
f"size={d_sz} {match}")
if ok:
print(" All entries consistent ✓")
# Also confirm global header px dimensions
gw = struct.unpack_from('<I', data, 0x7C)[0]
gh = struct.unpack_from('<I', data, 0x80)[0]
print(f"\nGlobal header px dimensions: {gw}×{gh} "
f"({'✓ correct for Mono 4' if gw==9024 and gh==5120 else '*** WRONG ***'})")

66
diagnose/probe_laye.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
probe_laye.py — find where 9024 and 5120 are actually stored in a pm4n file,
and dump LAYE + Mode sections in detail.
Run: python3 probe_laye.py Dummy.pm4n
"""
import struct, sys
path = sys.argv[1]
data = open(path, 'rb').read()
fsize = len(data)
print(f"File: {path} ({fsize} bytes)\n")
# Search for 9024 and 5120 as u32 LE, u16 LE, and as floats
targets = {
'u32 9024': struct.pack('<I', 9024),
'u32 5120': struct.pack('<I', 5120),
'u16 9024': struct.pack('<H', 9024),
'u16 5120': struct.pack('<H', 5120),
'f32 9024': struct.pack('<f', 9024.0),
'f32 5120': struct.pack('<f', 5120.0),
'f32 153.408': struct.pack('<f', 153.408),
'f32 87.040': struct.pack('<f', 87.040),
}
for label, needle in targets.items():
pos = 0
while True:
idx = data.find(needle, pos)
if idx < 0:
break
print(f" {label} found at 0x{idx:06X} ({idx})")
pos = idx + 1
print()
# Dump LAYE section
laye_off = data.find(b'LAYE')
if laye_off >= 0:
print(f"LAYE raw (first 160 bytes from 0x{laye_off:06X}):")
for i in range(0, 160, 16):
row = data[laye_off+i : laye_off+i+16]
if not row: break
hex_p = ' '.join(f'{b:02X}' for b in row)
interp = []
for j in range(0, len(row)-3, 4):
u = struct.unpack_from('<I', row, j)[0]
f = struct.unpack_from('<f', row, j)[0]
interp.append(f"0x{u:08X}/f={f:.5g}")
print(f" +0x{i:02X} {hex_p:<48} {' | '.join(interp)}")
print()
# Dump Mode section
mode_off = data.find(b'Mode')
if mode_off >= 0:
print(f"Mode raw (first 96 bytes from 0x{mode_off:06X}):")
for i in range(0, 96, 16):
row = data[mode_off+i : mode_off+i+16]
if not row: break
hex_p = ' '.join(f'{b:02X}' for b in row)
interp = []
for j in range(0, len(row)-3, 4):
u = struct.unpack_from('<I', row, j)[0]
f = struct.unpack_from('<f', row, j)[0]
interp.append(f"0x{u:08X}/f={f:.5g}")
print(f" +0x{i:02X} {hex_p:<48} {' | '.join(interp)}")
print()