#!/usr/bin/env python3 """Finaler PCC-Decoder fuer Kellogg's Tony and Friends (2026-07-22, ARIA). Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt, UND jetzt zusaetzlich Pixel-fuer-Pixel gegen echte DOSBox-Screenshots von KARTE.PCC und KELLOGGS.PCC verglichen -- siehe Bugfix 2 unten): - Byte 0-15: echter PCX-Header-Anfang (Manufacturer=0x0A, Version=5, Encoding=1/RLE, BPP=8, dann Xmin/Ymin/Xmax/Ymax als LE16 bei Offset 4-11). WICHTIG (Bugfix 1, 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 / Ymax-Ymin+1 berechnet werden. Die 2 LE16-Werte bei Offset 12-15 sehen fuer Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46, FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte. - Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten. WICHTIG (Bugfix 2, 2026-07-22): Die RLE-Runs werden NICHT pro Scanline zurueckgesetzt/abgeschnitten. Frueher wurde nach `width` Pixeln pro Zeile hart getrimmt und der Rest eines laufenden RLE-Runs verworfen ("row reset"). Das war FALSCH: dieses Format haelt sich nicht an die klassische PCX-Regel "ein Run ueberschreitet nie eine Scanline" -- Runs koennen frei ueber Zeilengrenzen hinweglaufen. Der Beweis: mit Row-Reset waren KARTE.PCC und KELLOGGS.PCC (die einzigen zwei echten 320x200-Vollbilder) sichtbar verwuerfelt (Charaktere an falscher Position, "Kerbe" im Logo-Rahmen), obwohl der Byte-Konsum fast vollstaendig war -- der Fehler kostet nur ~0.3-0.5% der Pixel, aber genau die falschen, wodurch ganze Bildbereiche sichtbar verrutschen. Mit kontinuierlicher Dekodierung (einfach `width*height` Pixel am Stueck aus dem RLE-Strom lesen, OHNE pro-Zeile zu trimmen) sind beide Bilder jetzt Pixel-fuer-Pixel identisch zu echten DOSBox-Screenshots (verifiziert per ImageMagick-Vergleich, siehe NOTES.md). Kleine Sprites waren von diesem Bug kaum betroffen, weil sie selten/nie einen Run ueber eine Zeilengrenze hinweg haben -- deshalb fiel es dort nicht auf. RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F), gefolgt von einem Wert-Byte. Sonst literaler Pixel. - Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette (klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette, keine globale Palette noetig. GELOEST (frueher "bekannter offener Bug", Stand vor 2026-07-22 Nachmittag): Das "Kerbe"/Notch-Artefakt neben dem Kellogg's-Schriftzug und die verrutschten Charaktere auf KARTE.PCC waren beide der gleiche Bug (Row-Reset, s.o.), NICHT ein horizontales Rollen und NICHT eine Palette-Fehlzuordnung. Mit der kontinuierlichen Dekodierung ist das Artefakt komplett weg. Nutzung: python3 pcc_to_png.py Schreibt ein PPM und konvertiert via ImageMagick `convert`/`magick` zu PNG. """ import sys, struct, subprocess, os, shutil def decode_pcc(data): manuf, version, encoding, bpp = data[0], data[1], data[2], data[3] xmin, ymin, xmax, ymax = struct.unpack('= n: break val = pixel_region[i]; i += 1 remaining = total - len(out) out.extend([val] * min(count, remaining)) else: out.append(b) if len(out) < total: out.extend([0] * (total - len(out))) return dict(width=width, height=height, marker_ok=(marker == 0x0C), pixels=bytes(out), palette=palette, consumed=i, pixel_region_len=n) def write_png(pixels, palette, width, height, out_png): ppm = out_png + '.ppm' with open(ppm, 'wb') as f: f.write(f'P6\n{width} {height}\n255\n'.encode()) buf = bytearray() for px in pixels[:width*height]: r, g, b = palette[px] buf.extend([r, g, b]) f.write(bytes(buf)) convert_bin = shutil.which('magick') or shutil.which('convert') args = [convert_bin, ppm, out_png] if 'magick' not in (convert_bin or '') or convert_bin.endswith('convert') else [convert_bin, 'convert', ppm, out_png] subprocess.run(args, check=True) os.remove(ppm) def main(): inp, outp = sys.argv[1], sys.argv[2] with open(inp, 'rb') as f: data = f.read() res = decode_pcc(data) print(f"{inp}: {res['width']}x{res['height']} marker_ok={res['marker_ok']} " f"consumed={res['consumed']}/{res['pixel_region_len']}") write_png(res['pixels'], res['palette'], res['width'], res['height'], outp) print(f"-> {outp}") if __name__ == '__main__': main()