Dateiformat .des von Texcelle umwandeln

Ich spiele mal Totengräber, weil das ja vielleicht irgendwann für irgendwen interessant sein könnte ... :)
[src=python]import sys, struct
from collections import namedtuple
from PIL import Image

class DesDataChunk:
__slots__ = ('tag', 'payload')
def __init__(self, tag, payload):
self.tag = tag
self.payload = payload

class DesTreeChunk:
__slots__ = ('tag', 'nodes')
def __init__(self, tag, nodes):
self.tag = tag
self.nodes = nodes
def __getitem__(self, tag):
nodes = [c for c in self.nodes if c.tag == tag]
if len(nodes) == 0:
return None
elif len(nodes) == 1:
return nodes[0]
else:
return tuple(nodes)

DesPathElement = namedtuple('DesPathElement', 'chunk end_pos')

file_name = sys.argv[1]

with open(file_name, 'rb') as desfile:
desdata = desfile.read()

des = DesTreeChunk('', [])
despath = [ DesPathElement(des, len(desdata)) ]
fp = 0
while fp < len(desdata):
tag = desdata[fp:fp+4].decode('ascii')
fp += 4
flags = struct.unpack('BBBB', desdata[fp:fp+4])
fp += 4
payload_len = struct.unpack('<I', desdata[fp:fp+4])[0]
fp += 4
indent = "\t" * (len(despath)-1)
if flags[0] == 0:
chunk = DesTreeChunk(tag, [])
despath[-1].chunk.nodes.append(chunk)
despath.append(DesPathElement(chunk, fp+payload_len))
elif flags[0] == 1:
payload = desdata[fp:fp+payload_len]
fp += payload_len
chunk = DesDataChunk(tag, payload)
despath[-1].chunk.nodes.append(chunk)
else:
raise ValueError('parse error, expected flag[0] to be 0 or 1, got '+flags[0])
print(f'{indent}chunk: {tag} - flags={flags} ==> {type(chunk)} ({payload_len} bytes)')
while despath and fp >= despath[-1].end_pos:
despath.pop()

assert despath == []

print('Structure OK, reading image data ...')
dhdr = des['CPAT']['CDIB']['DHDR'].payload
(img_width, img_height, img_depth, img_resx, img_resy) = struct.unpack('<IIHII', dhdr)

assert img_depth == 8

img_palette = []
dpal = des['CPAT']['CDIB']['DPAL'].payload
for i in range(0, 256):
# color order = b, g, r
img_palette.extend(dpal[4+i*4:4+i*4+3][::-1])

ddat = des['CPAT']['CDIB']['DDAT'].payload
img_data = bytearray()
fp = 4
while fp < len(ddat):
rle_compressed_len = struct.unpack('<I', ddat[fp:fp+4])[0]
fp += 4
rle_uncompressed_len = struct.unpack('<I', ddat[fp:fp+4])[0]
fp += 4
rle_data = ddat[fp:fp+rle_compressed_len]
fp += rle_compressed_len
uncompressed_data = bytearray()
cp = 0
while cp < rle_compressed_len:
if rle_data[cp] == 0xFF:
run_len = rle_data[cp+1]
if run_len == 0:
run_len = 256
run_byte = rle_data[cp+2]
cp += 3
uncompressed_data += bytearray((run_byte,)) * run_len
else:
uncompressed_data += bytearray((rle_data[cp],))
cp += 1

assert len(uncompressed_data) == rle_uncompressed_len
#print(f'RLE; compressed={rle_compressed_len}, uncompressed={rle_uncompressed_len}')
img_data += uncompressed_data

if img_width % 4 > 0:
img_width += 4 - img_width % 4

print('Saving TIFF ...')
img = Image.frombytes('P', (img_width, img_height), bytes(img_data)).transpose(Image.FLIP_LEFT_RIGHT)
img.putpalette(img_palette)
img.save(file_name.replace('.des', '.tiff'))
[/src]

Das Ergebnis ist so was:


Das Format ist nicht so schrecklich kompliziert, die Grundstruktur ist ein Baum von Chunks/Blöcken. Jeder Block hat einen Namen (4 Zeichen) und enthält entweder Daten oder Unterblöcke. Die eigentlichen Bilddaten stehen im Block DDAT und sind per RLE komprimiert.
Was hilft, sobald man die Grundstruktur der Blöcke erkannt hat: Blocknamen googeln. Dann findet man sowas: (Backup im Spoiler, falls das irgendwann verschwindet). Damit ist der Rest ein Spaziergang :D Der Python-Code behandelt aber nur die Fälle, die in Woodstocks Beispiel auch verwendet werden - also eine unkomprimierte Datei mit 256 Farben und Farbpalette. Das Ganze gibt's nach der Beschreibung aber auch noch in RGB und in komprimiert ...

The graphic file format contains a data block which stores a thumbnail image of the image stored in the file, so the entire file does not have to be read. The image data in the thumbnail is stored in either 8-bit indexed-color or 24-bit true-color modes and a data block in the file will specify the bit depth. If the file contains an 8-bit indexed-color image, a data block in the file will contain a color palette of 256 entries. The indexed-color image is stored run-length encoded and the true-color image is stored in BGR byte order. The pixels of the image have an aspect-ratio. A data block will give the resolution in the X and the resolution in the Y of the image as pixels per unit. If the aspect ratio is not square, the thumbnail image created will have to be interpolated using Supersampling to display an image with the correct aspect ratio. The file can be either compressed or uncompressed. A data block at the beginning of the file will determine if compression if used. If the file is compressed, the next data block will contain a Zip compressed version of the uncompressed file.

The file consists of data blocks stored in Little Endian encoding. The first 12 bytes of the block are a header that describe the block and give an offset to the end of the block. The first 4 bytes of the header are ASCII characters that name the block. The next 4 bytes are flags. If the first byte flag if a 1, then data follows the header. If the first flag is 0, nested blocks follow the header. The last 4 bytes in the header are an unsigned integer offset, in bytes, to the end of the block.

The first block in the file is the NIFF block; it specifies if the file is compressed or uncompressed. If the data in the block is ZNIF, the file is compressed. If the data is DSGN, the file is uncompressed. If the file is compressed, the second block in the file will be the ZNIF block; It contains 2 blocks. The second nested block is the ZCHK block. It contains Zip compressed data of the uncompressed version of the file.

The second data block in the uncompressed file is the STMP block. It stored a thumbnail of the image in the file that will be used by the shell extension. The first block inside of the STMP block is the CDIB block that contains the image data of the thumbnail. The first block inside if the CDIB block is the DHDR block which contains header information for the thumbnail.

The data in the DHDR block is as follows. The first 4 bytes are the width of the thumbnail image in pixels. The next 4 bytes are the height of the thumbnail image in pixels. The next 2 bytes are the bit-depth of the image, 8 for indexed-color and 24 for true-color. The next 4 blocks are the resolution of the image in X, and the last 4 bytes are the resolution of the image in Y. The resolution of the image is the number of pixels per square unit and will be used to determine the pixel-aspect-ratio of the image. When creating the image for the thumbnail, if the image is not in square aspect-ratio, the resulting thumbnail will have to either be scaled down alond one of the axes using Supersampling, or scaled up along the other axis using linear interpolation to produce an image that is visually correct.

The next data block inside of the CDIB block, if the image is 8-bit indexed-color, is the DPAL block that contains the palette for the image. The first 4 bytes are flags followed by 256 4-byte records containing bytes for the color values blue, green, then red. The last byte in the record is 0. If the image is 24-bit true-color, the DPAL block will be missing.

The next block in the CDIB block is the DDAT block that contains the image data. The DDAT block begins with 4 bytes of flags and contains the image data for either the 8-bit indexed color pixels, or the 24-bit true-color pixels. The pixels are stores in rows which are read from left to right, and the rows are read from the bottom up. Each row is padded to be divisible by 4 bytes, so if the image is 13 pixels wide, in indexed-color it would be 16 bytes in stride, and in true-color would be 40 bytes in stride. If the image is 24-bit true-color, the pixels are stored as 3 bytes in BGR order. If the image is 8-bit indexed-color, the pixels are stored in run-length-encoded blocks. Each block begins with an 8 byte header of which the first 4 are the length of the block in bytes and the second 4 are the number of bytes in the uncompressed block. After the header, each block contains the bytes of the color indexes, or run-length-encoded sections that begin with FF followed by the length of the run, followed by the byte for the color index. If the length of the run is 0, the run is 256.
 
Zuletzt bearbeitet:
  • Thread Starter Thread Starter
  • #22
Ja, der Hammer, genau das sollte rauskommen.
Da es sich nur um Indizierte Grafiken handelt, sind wir schon durch.

Ich würde den Code gerne "klauen" oder magst mir was basteln, entweder mit Eingabemaske oder CLI Command.

Ich sage mal Vielen lieben Dank und kein Problem für das Schänden.

Gruß
Woodstock
 
Klar, kein Problem. Ich hab den Code oben mal eben so angepasst dass der Dateiname per Argument übergeben werden kann. Der Output landet dann in einer png-Datei mit gleichem Namen. Unter Windows kannst du die des einfach per Drag&Drop auf das Script ziehen.

Falls du kein Python installiert hast, hier mit PyInstaller in eine exe gepackt:
 
  • Thread Starter Thread Starter
  • #24
Jo, was soll ich dazu sagen, promte Lieferung.

Eine kleine Kleinigkeit haben wir noch, nun ja eigentlich zwei:

1. Die ausgegebene PNG Datei ist spiegelverkehrt.
2. ich hätte die Ausgabedatei gern als TIF.

Hier mal ne Handvoll .des Dateien zum spielen:

P.S. die "Probleme" kann ich lösen, als Exe wäre es jedoch perfekt.

Gruß
Woodstock
 
So, nachdem ich den Code gestern Abend angepasst hatte, bin ich nun endlich auch dazu gekommen, auf einer Windows-Kiste eine neue exe zu bauen:
Aber Vorsicht, die TIFFs werden mitunter sehr gross, weil unkomprimiert (~200 MB bei deiner ersten DES).

Wenn du eine DES-Datei hast, die nicht (mehr oder weniger) symmetrisch ist, wärs einfacher zu sehen ob nun die Ausrichtung stimmt. Blöde Bildformate, die Pixel nicht von links oben nach rechts unten lesen :)
 
Super dass du das Thema mit deiner Lösung aufgegriffen hast. Aber mal eine Anmerkung zum Code - ich würde einen Parameter mitgeben ob "left to right" oder "right to left" (respektiv top bottom, bottom top) gelesen werden soll bzw. eher ob das "Image Transpose" aufgerufen werden soll oder nicht. Der Parameter macht das etwas flexibler.

Im weiteren würde ich die sys.argv testen ob wirklich ein Parameter angegeben wurde und wenn ein Dateiname übergeben wurde, die Datei existiert. (os exists). Oder eben das aktuelle Workdir komplett gelesen und vearbeitet werden soll in dem das Skript aufgerufen wird.

Und zu guter letzt - Output nach entweder "PNG" oder "TIFF" - auch über einen möglichen Parameter gesteuert, so das Default Tiff wäre, aber PNG auch möglich ist.
 
In Bezug auf das Errorhandling hast du natürlich Recht, die Fehlermeldungen könnten informativer sein, wenn du kein Argument übergibst kommt aktuell ein IndexError raus, wenn die Datei nicht existiert ein FileNotFoundError. Ob man das vorab überprüft, oder erst annimmt das alles passt und ggf. die Exceptions behandelt ist eine eher philosophische Frage, wobei Python letzteres empfiehlt: und
Auf jedem Fall sollte ich die Exceptions aber eigentlich abfangen und schönere Fehlermeldungen produzieren. Konsequenterweise insbesondere auch dann, wenn die Baumstruktur im File nicht so ist wie ich sie erwarte, also gewisse Chunks fehlen, die Bildgrösse im Header nicht mit der Grösse der Daten übereinstimmt, das RLE-Decoding fehlschlägt, etc. Momentan fliegt da im Zweifel eine eher nichts sagende Exception (Assertions sind ja schon da). Und das Parsing würde eigentlich in eine Library gehören und sollte vom Ezeugen des Outputs unabhängig sein. War halt nur mal schnell hingehackt :)

Wegen der Argumente, ich habe das absichtlich so gemacht dass genau ein Argument übergeben wird, damit die Windows-User unter uns DES-Dateien per Drag&Drop auf das Script ziehen können und eben nicht jedes Mal explizit die Kommandozeile bemühen müssen. In diesem Fall übergibt Windows dem Skript halt genau ein Argument. Ansonsten hätte ich einen weiteren Parameter für die Ausgabedatei gemacht. Gut, könnte man natürlich optional machen, wenn da verwenden, wenn nein aus dem Eingabenamen ableiten.
Die Reihenfolge der Pixel ist andererseits aber durch das DES-Format gegeben, da sehe ich eher weniger Nutzen für einen Parameter. Das sollte man einmal richtig hinkriegen und gut. Ich bin mir nur nicht sicher, ob das jetzt passt, weil Woodstocks Dateien alle sehr symmetrisch ausschauen. Da ist dann schwer zu beurteilen, ob die gerade spiegelverkehrt oder 180° gedreht sind...
 
Zurück
Oben