episode.py

  1. # python j2e 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. class Episode:
  15.     _headerstruct = "L|headersize/L|position/L|isregistered/L|unknown/s128|episodename/s32|firstlevel/L|width/L|height/L|unknown2/L|unknown3/L|titlewidth/L|titleheight/L|unknown3/L|unknown4/L|unknown5"
  16.     _headersize = 208
  17.     background = (87, 0, 203) #default background, (r, g, b)
  18.     header = episodefile = palette = filesize = None
  19.  
  20.     def __init__(self, filename):
  21.         ''' loads file contents into memory '''
  22.         try:
  23.             self.episodefile = open(filename, "rb")
  24.         except:
  25.             print("file %s could not be read!" % filename)
  26.             os._exit(0)
  27.            
  28.         self.filesize = os.path.getsize(filename)
  29.         self.episodefile = self.episodefile.read(self.filesize)
  30.        
  31.     def get_stream(self, streamnum):
  32.         ''' uncompresses one of the substreams (first stream = streamnum 1, and so on) '''
  33.         offset = self._headersize + 4
  34.         for i in range(1,streamnum):
  35.             offset += self.header["c"+str(i)] + 4
  36.  
  37.         chunk = self.episodefile[offset:offset+self.header["c"+str(streamnum)]]
  38.         return zlib.decompressobj().decompress(chunk, self.header["u"+str(streamnum)])
  39.        
  40.     def get_palette(self, given = None):
  41.         ''' creates palette from JASC palette - should be Jazz2 menu palette for episode files. '''
  42.         if not self.palette:
  43.             palfile = open("Jazz2.pal").readlines() if not given else given
  44.             pal = list()
  45.             for i in range(3, 259): #first 3 lines = meta-info
  46.                 color = palfile[i].rstrip("\n").split(' ')
  47.                 pal.append((int(color[0]), int(color[1]), int(color[2])))
  48.             self.palette = pal
  49.        
  50.         return self.palette
  51.    
  52.     def read_header(self):
  53.         ''' reads and parses header, and calculates zlib stream offsets '''
  54.         if not self.header:
  55.             self.header = named_unpack(self._headerstruct, self.episodefile)
  56.             self.header["u1"] = self.header["width"] * self.header["height"] #uX = uncompressed size for stream X, cX = compressed
  57.             self.header["u2"] = self.header["u3"] = self.header["titlewidth"] * self.header["titleheight"]
  58.             self.header["c1"] = struct.unpack("L", self.episodefile[self._headersize:self._headersize+4])[0]
  59.             offset = self._headersize + 4 + self.header["c1"]
  60.             self.header["c2"] = struct.unpack("L", self.episodefile[offset:offset+4])[0]
  61.             offset += self.header["c2"] + 4
  62.             self.header["c3"] = struct.unpack("L", self.episodefile[offset:offset+4])[0]
  63.            
  64.         return self.header
  65.        
  66.     def render_image(self, stream, width, height):
  67.         ''' renders one of the images. '''
  68.         if not self.header:
  69.             self.read_header()
  70.        
  71.         #prepare image
  72.         preview = Image.new("RGB", (width, height))
  73.         ImageDraw.Draw(preview).rectangle([(0,0), (width, height)], self.background)
  74.         pixelmap = self.get_stream(stream)
  75.         pal = self.get_palette()
  76.        
  77.         #for faster parsing (since these are called for every pixel)
  78.         up = struct.unpack
  79.         im = preview.load()
  80.  
  81.         #put pixels
  82.         for y in range(0, height):
  83.             for x in range(0, width):
  84.                 p = (y * width) + x
  85.                 color = up("B", pixelmap[p:p+1])[0]
  86.                 if color > 0:
  87.                     im[x, y] = pal[color]
  88.  
  89.         return preview
  90.    
  91.     def render_illustration(self):
  92.         ''' renders episode illustration image. '''
  93.         if not self.header:
  94.             self.read_header()
  95.  
  96.         return self.render_image(1, self.header["width"], self.header["height"])
  97.  
  98.     def render_title_bright(self):
  99.         ''' renders bright (highlighted) version of episode title image. '''
  100.         if not self.header:
  101.             self.read_header()
  102.  
  103.         return self.render_image(2, self.header["titlewidth"], self.header["titleheight"])
  104.  
  105.     def render_title_dark(self):
  106.         ''' renders dark version of episode title image. '''
  107.         if not self.header:
  108.             self.read_header()
  109.  
  110.         return self.render_image(3, self.header["titlewidth"], self.header["titleheight"])
  111.  
  112. def named_unpack(format, string):
  113.     ''' hacky wrapper for struct.unpack() that allows for easier format specification '''
  114.     format = re.sub("[^0-9a-zA-Z?/|]", "", format)
  115.     sizes = {"x": 1, "c": 1, "b": 1, "B": 1, "?": 1, "h": 2, "H": 2, "i": 4, "I": 2, "l": 4, "L": 4, "q": 8, "Q": 8, "f": 0, "d": 0} #don"t use d or f!
  116.     items = format.split("/")
  117.     ret = dict()
  118.     for item in items:
  119.         base = item.split("|")
  120.         count = re.sub("[^0-9]", "", base[0])
  121.         byte = str(re.sub("[^a-zA-Z?]", "", base[0]))
  122.         count = int(count) if len(count) != 0 else 1
  123.         if byte not in ["p", "s"]:
  124.             for i in range(1, count+1):
  125.                 sub = string[0:sizes[byte]]
  126.                 suffix = str(i) if count > 1 else ""
  127.                 key = base[1]+suffix if len(base[1]) > 0 else int(suffix)
  128.                 ret[key] = struct.unpack("<" + byte, sub)[0]
  129.                 string = string[sizes[byte]:]
  130.         else:
  131.             sub = string[0:count]
  132.             if byte != "x":
  133.                 ret[base[1]] = struct.unpack("<" + str(count) + byte, sub)[0]
  134.             string = string[count:]
  135.  
  136.     return ret
  137.  
  138. j2e = Episode("Flash.j2e")
  139. j2e.render_illustration().save("preview.png", "PNG")