decode_tex.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. #!/usr/bin/env python3
  2. """Decodes a Wallpaper Engine .tex file's first mipmap into a plain image file.
  3. Only handles the common case: TEXV0005/TEXI0001 header, TEXB0003/TEXB0004 container,
  4. FreeImage-encoded payload (FIF_PNG in every sample seen so far) - i.e. the mipmap bytes
  5. are themselves a complete PNG/etc file, not raw pixel data. Doesn't touch mipmaps beyond
  6. the first, animated (TEXS*) textures, or DXT/BC-compressed formats.
  7. Usage:
  8. decode_tex.py <path-to.tex> <output-image-path>
  9. Needs: pip install lz4 (only when the mipmap is LZ4-compressed, i.e. most of them).
  10. """
  11. import struct
  12. import sys
  13. try:
  14. import lz4.block
  15. except ImportError:
  16. lz4 = None
  17. def decode(path, out_path):
  18. f = open(path, 'rb')
  19. magic1 = f.read(9)
  20. assert magic1[:8] == b'TEXV0005', magic1
  21. magic2 = f.read(9)
  22. assert magic2[:8] == b'TEXI0001', magic2
  23. fmt, flags, tex_w, tex_h, w, h = struct.unpack('<6I', f.read(24))
  24. f.read(4) # ignored trailing header field
  25. container_magic = f.read(9)
  26. image_count = struct.unpack('<I', f.read(4))[0]
  27. fif = None
  28. if container_magic[:8] in (b'TEXB0003', b'TEXB0004'):
  29. fif = struct.unpack('<I', f.read(4))[0]
  30. if container_magic[:8] == b'TEXB0004':
  31. f.read(4) # isVideoMp4 flag
  32. print('container', container_magic[:8], 'fif', fif, 'imageCount', image_count, 'declaredSize', w, h)
  33. mipmap_count = struct.unpack('<I', f.read(4))[0]
  34. print('mipmapCount', mipmap_count)
  35. mip_w, mip_h = struct.unpack('<II', f.read(8))
  36. compression, uncompressed_size, compressed_size = struct.unpack('<Iii', f.read(12))
  37. print('mip0', mip_w, mip_h, 'compression', compression,
  38. 'uncompressedSize', uncompressed_size, 'compressedSize', compressed_size)
  39. if compression == 0:
  40. # misnamed in the engine's own parser too: compressedSize holds the real length here
  41. uncompressed_size = compressed_size
  42. data = f.read(uncompressed_size)
  43. elif compression == 1:
  44. if lz4 is None:
  45. raise SystemExit("mipmap is LZ4-compressed - install with: pip install lz4")
  46. comp = f.read(compressed_size)
  47. data = lz4.block.decompress(comp, uncompressed_size=uncompressed_size)
  48. else:
  49. raise SystemExit(f"unknown compression mode {compression}")
  50. open(out_path, 'wb').write(data)
  51. print('wrote', out_path, len(data), 'bytes, first bytes:', data[:8])
  52. if __name__ == '__main__':
  53. if len(sys.argv) != 3:
  54. print(__doc__)
  55. raise SystemExit(1)
  56. decode(sys.argv[1], sys.argv[2])