j2a.py

  1. # python j2a parser
  2. # by stijn
  3. # thanks to neobeo/j2nsm for the file format specs
  4. # see http://www.jazz2online.com
  5.  
  6. import struct
  7. import os
  8. import zlib
  9. import math
  10. #needs python image library, http://www.pythonware.com/library/pil/
  11. import Image
  12. import ImageDraw
  13.  
  14. import misc
  15.  
  16. class J2A:
  17.     _headerstruct = "s4|signature/L|magic/L|headersize/h|version/h|unknown/L|filesize/L|crc32/L|setcount"
  18.     _animheaderstruct = "s4|signature/B|animcount/B|samplecount/h|framecount/l|unknown/l|c1/l|u1/l|c2/l|u2/l|c3/l|u3/l|c4/l|u4"
  19.     _animinfostruct = "H|framecount/H|fps/l|reserved"
  20.     _frameinfostruct = "H|width/H|height/h|coldspotx/h|coldspoty/h|hotspotx/h|hotspoty/H|gunspotx/H|gunspoty/L|imageoffset/L|maskoffset"
  21.     _headersize = 28
  22.     header = setdata = setoffsets = palette = None
  23.     currentset = -1
  24.  
  25.     def __init__(self, filename = "Anims.j2a"):
  26.         ''' loads file contents into memory '''
  27.         try:
  28.             self.j2afile = open(filename, "rb")
  29.         except:
  30.             print("file %s could not be read!" % filename)
  31.             os._exit(0)
  32.            
  33.         self.filesize = os.path.getsize(filename)
  34.         self.j2afile = self.j2afile.read(self.filesize)
  35.        
  36.     def get_substream(self, streamnum):
  37.         offset = self.setoffsets[self.currentset]
  38.         data = self.setdata[self.currentset]
  39.         suboffset = offset + 44
  40.         for i in range(1, streamnum):
  41.             suboffset += data["c" + str(i)]
  42.  
  43.         chunk = self.j2afile[suboffset:suboffset+data["c" + str(streamnum)]]
  44.         return zlib.decompressobj().decompress(chunk, data["u" + str(streamnum)])
  45.    
  46.     def read_header(self):
  47.         ''' reads and parses header '''
  48.         if not self.header:
  49.             self.header = misc.named_unpack(self._headerstruct, self.j2afile[:self._headersize])
  50.             setlength = self.header["setcount"] * 4
  51.             self.setdata = list()
  52.             self.setoffsets = list()
  53.             for i in range(0, self.header["setcount"]):
  54.                 offset = struct.unpack("L", self.j2afile[self._headersize+(i*4):self._headersize+(i*4)+4])[0]
  55.                 self.setoffsets.append(offset)
  56.                 self.setdata.append(misc.named_unpack(self._animheaderstruct, self.j2afile[offset:offset+44]))
  57.            
  58.         return self.header
  59.        
  60.     def load_set(self, setnum):
  61.         if not self.header:
  62.             self.read_header()
  63.        
  64.         if -1 < setnum < self.header["setcount"]:
  65.             self.currentset = setnum
  66.         else:
  67.             print("set %s doesn't exist!" % setnum)
  68.             os._exit(0)
  69.        
  70.     def get_palette(self, given = None):
  71.         if not self.palette:
  72.             palfile = open("Jazz2.pal").readlines() if not given else given
  73.             pal = list()
  74.             for i in range(3, 251):
  75.                 color = palfile[i].rstrip("\n").split(' ')
  76.                 pal.append((int(color[0]), int(color[1]), int(color[2])))
  77.             self.palette = pal
  78.        
  79.         return self.palette
  80.            
  81.     def make_pixelmap(self, raw):
  82.         width, height = struct.unpack("HH", raw[0:4])
  83.         if width >= 32768:
  84.             width -= 32768 #unset msb
  85.         raw = raw[4:]
  86.         #prepare pixelmap
  87.         map = list()
  88.         for i in range(0, width):
  89.             map.append(list())
  90.             for j in range(0, height):
  91.                 map[i].append(0)
  92.         #fill it with data! (image format parser)
  93.         length = len(raw)
  94.         up = struct.unpack
  95.         x = y = o = 0
  96.         for i in range(0, length):
  97.             i += o
  98.             byte = up("B", raw[i])[0]
  99.             if x > width or y > height:
  100.                 break
  101.             if byte > 128:
  102.                 sub = raw[i+1:i+byte-127]
  103.                 for j in range(0, byte-128):
  104.                     try:
  105.                         map[x][y] = up("B", sub[j])[0]
  106.                     except:
  107.                         break
  108.                     o += 1
  109.                     x += 1
  110.             elif byte < 128:
  111.                 x += byte
  112.             else:
  113.                 x = 0
  114.                 y += 1
  115.         return map
  116.    
  117.     def render_pixelmap(self, pixelmap):
  118.         width, height = (len(pixelmap), len(pixelmap[0]))
  119.         img = Image.new("RGBA", (width, height))
  120.         im = img.load()
  121.         pal = self.get_palette()
  122.  
  123.         for x, row in enumerate(pixelmap):
  124.             for y, index in enumerate(row):
  125.                 if index > 1:
  126.                     im[x, y] = pal[index]
  127.        
  128.         return img
  129.        
  130.     def get_frame(self, coordinates):
  131.         ''' gets frame: coordinates should be a tuple (set, animation, frame) '''
  132.         if not self.header:
  133.             self.read_header()
  134.            
  135.         setnum, animnum, framenum = coordinates
  136.         self.load_set(setnum)
  137.         data = self.setdata[setnum]
  138.         animinfo = self.get_substream(1)
  139.         frameinfo = self.get_substream(2)
  140.         frameoffset = 0
  141.         for i in range(0, animnum):
  142.             try:
  143.                 info = misc.named_unpack(self._animinfostruct, animinfo[i*8:(i*8)+8])
  144.             except:
  145.                 print "couldnt load frame at coordinates %s" % repr(coordinates)
  146.                 return
  147.             frameoffset += info["framecount"]
  148.         info = misc.named_unpack(self._frameinfostruct, frameinfo[frameoffset*24:(frameoffset*24)+24])
  149.         dataoffset = info["imageoffset"]
  150.         imagedata = self.get_substream(3)
  151.        
  152.         pixelmap = self.make_pixelmap(imagedata[dataoffset:])
  153.         return [info, self.render_pixelmap(pixelmap)]
  154.        
  155. j2a = J2A()
  156. j2a.render_frame((67, 12, 0)).save("preview.png", "PNG")