class.Episode.php

  1. <?php
  2. /**
  3.  * class.Episode.php
  4.  *
  5.  * Provides an interface to read episode (J2E) files.
  6.  *
  7.  * LICENSE:
  8.  *
  9.  *             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  10.  *                       Version 2, December 2004
  11.  *
  12.  * Copyright (C) 2009 Stijn Peeters
  13.  *
  14.  * Everyone is permitted to copy and distribute verbatim or modified
  15.  * copies of this license document, and changing it is allowed as long
  16.  * as the name is changed.
  17.  *
  18.  *             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  19.  * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
  20.  *
  21.  * 0. You just DO WHAT THE FUCK YOU WANT TO.
  22.  *
  23.  * @package    J2Ov4
  24.  * @author     Stijn Peeters
  25.  * @copyright  Copyright (c) 2009, Stijn Peeters
  26.  * @version    1.0
  27.  * @link       http://www.stijnpeeters.nl/
  28.  * @license    http://sam.zoy.org/wtfpl/ Do What The Fuck You Want To Public License
  29.  */
  30.  
  31.  /**
  32.  * Episode class
  33.  *
  34.  * Reads and interprets Jazz Jackrabbit 2 Episode files
  35.  *
  36.  * @package    J2Ov4
  37.  * @author     Stijn Peeters
  38.  * @copyright  Copyright (c) 2009, Stijn Peeters
  39.  * @version    1.0
  40.  */
  41. class Episode {
  42.         /**
  43.          * @var  string  The episode file to read from
  44.          * @access private
  45.          */
  46.         private $_Resource;
  47.         /**
  48.          * @var  array   File header, parsed
  49.          * @access private
  50.          */
  51.         private $aHeader = array();
  52.         /**
  53.          * @var  array   Palette, each index is an array(r, g, b)
  54.          * @access private
  55.          */
  56.         private $aPalette = array();
  57.         /**
  58.          * @var  boolean Magic variable to check whether file can be loaded or not.
  59.          * @access public
  60.          */
  61.         public  $bValid = false;
  62.         /**
  63.          * @var  array   Byte lengths for substreams. There are 3 substreams; key "u1"
  64.          *               holds the uncompressed length of stream 1, "c1" the compressed
  65.          *               length, and so on.
  66.          * @access private
  67.          */
  68.         private $aStreamSizes = array();
  69.         /**
  70.          * @var array Default background color ("JCS Blue") - format: array(r, g, b)
  71.          * @access private
  72.          */
  73.         private $aBackground = array(87, 0, 203);
  74.         /**
  75.          * Palette file to use. Should be a JASC palette (.pal)
  76.          */
  77.         const PALETTE_FILE = 'Jazz2.pal';
  78.         /**
  79.          * Size of the file header
  80.          */
  81.         const HEADER_SIZE = 208;
  82.         /**
  83.          * Header structure
  84.          * @see PHP_MANUAL#pack()
  85.          */
  86.         const HEADER_STRUCT = 'Vheadersize/Vposition/Visregistered/Vunknown1/a128episodename/a32firstlevel/Vwidth/Vheight/Vunknown2/Vunknown3/Vtitlewidth/Vtitleheight/Vunknown5/Vunknown6';
  87.        
  88.         /**
  89.          * Constructor method
  90.          *
  91.          * Sets up the object, checking whether given file path is valid and giving several
  92.          * variables initial values.
  93.          *
  94.          * @param   string  $sFilename  The file path pointing to the episode to use.
  95.          *
  96.          * @uses    Episode::$_Resource To store the episode file for reading.
  97.          * @uses    Episode::$bValid    To store whether the file could be read or not.
  98.          *
  99.          * @access  public
  100.          */
  101.         public function __construct($sFilename) {
  102.                 if(!is_readable($sFilename)) {
  103.                         //try some alternative file names
  104.                         $aFilename = explode('/', $sFilename);
  105.                         $sTempName = array_pop($aFilename);
  106.                         $sPath = implode('/', $aFilename);
  107.                         if(is_readable($sPath.'/'.strtoupper($sTempName))) {
  108.                                 $sFilename = $sPath.'/'.strtoupper($sTempName);
  109.                         } elseif(is_readable($sPath.'/'.strtolower($sTempName))) {
  110.                                 $sFilename = $sPath.'/'.strtolower($sTempName);
  111.                         } elseif(is_readable($sPath.'/'.ucfirst($sTempName))) {
  112.                                 $sFilename = $sPath.'/'.ucfirst($sTempName);
  113.                         } else {
  114.                                 return false;
  115.                         }
  116.                 }
  117.                
  118.                 $this->bValid = is_readable($sFilename);
  119.                 $this->_Resource = file_get_contents($sFilename);
  120.         }
  121.        
  122.         /**
  123.          * Retrieve episode header
  124.          *
  125.          * The episode file header contains mostly file sizes and two strings. The header
  126.          * is parsed and stored in an array for later usage. While not strictly part of
  127.          * the header, the sizes of the file image data streams (which are stored in the
  128.          * four bytes perceding each stream) and the uncompressed size (one byte per
  129.          * pixel) are also calculated or retrieved and stored.
  130.          *
  131.          * @uses    Episode::$aStreamSizes To store the stream sizes found.
  132.          * @uses    Episode::$aHeader   To store the episode header.
  133.          * @uses    Episode::$_Resource To read from the episode file.
  134.          *
  135.          * @access  private
  136.          */
  137.         public function getHeader() {
  138.                 if(empty($this->aHeader)) {
  139.                         $this->aHeader = unpack(self::HEADER_STRUCT, substr($this->_Resource, 0, self::HEADER_SIZE));
  140.                         $this->aStreamSizes['u1'] = $this->aHeader['width'] * $this->aHeader['height'];
  141.                         list(,$this->aStreamSizes['c1']) = unpack('V',substr($this->_Resource, self::HEADER_SIZE, 4));
  142.                         $this->aStreamSizes['u2'] = $this->aHeader['titlewidth'] * $this->aHeader['titleheight'];
  143.                         $this->aStreamSizes['u3'] = $this->aHeader['titlewidth'] * $this->aHeader['titleheight'];
  144.                         list(,$this->aStreamSizes['c2']) = unpack('V',substr($this->_Resource, (self::HEADER_SIZE + 4 + $this->aStreamSizes['c1']), 4));
  145.                         list(,$this->aStreamSizes['c3']) = unpack('V',substr($this->_Resource, (self::HEADER_SIZE + 8 + $this->aStreamSizes['c1'] + $this->aStreamSizes['c2']), 4));
  146.                 }
  147.                
  148.                 return $this->aHeader;
  149.         }
  150.        
  151.         /**
  152.          * Get episode name
  153.          *
  154.          * Determines the episode's name as it shows up in the file.
  155.          *
  156.          * @returns string              The episode name. White spaces and NULs are trimmed off.
  157.          *
  158.          * @access  public
  159.          */
  160.         public function getName() {
  161.                 return trim(substr($this->_Resource,16,128));
  162.         }
  163.        
  164.         /**
  165.          * Retrieve file substream
  166.          *
  167.          * Retrieves one of the data streams in the file. The data streams are compressed
  168.          * images (one byte per pixel). The uncompressed data stream is returned.
  169.          *
  170.          * @param   integer $iStream    The stream to retrieve. Should be between 1 and 3
  171.          *                              (inclusive).
  172.          *
  173.          * @return  string              The uncompressed data stream (image data).
  174.          *
  175.          * @uses    Episode::getHeader() To calculate stream sizes, if needed.
  176.          * @uses    Episode::$aStreamSizes To retireve stream sizes.
  177.          * @uses    Episode::$_Resource To read compressed stream data
  178.          *
  179.          * @access private
  180.          */
  181.         private function getStream($iStream) {
  182.                 if(empty($this->aHeader)) {
  183.                         $this->getHeader();
  184.                 }
  185.                
  186.                 $iOffset = self::HEADER_SIZE + 4;
  187.                 for($i = 1; $i < $iStream; $i++) {
  188.                         $iOffset += $this->aStreamSizes['c'.$i] + 4;
  189.                 }
  190.                
  191.                 return gzuncompress(substr($this->_Resource, $iOffset, $this->aStreamSizes['c'.$iStream]), $this->aStreamSizes['u'.$iStream]);
  192.         }
  193.  
  194.         /**
  195.          * Generate episode image
  196.          *
  197.          * Generates an image based on one of the image streams in the episode file.
  198.          *
  199.          * @param   integer $iStream    The stream number to render. Should be between
  200.          *                              1 and 3 (inclusive)
  201.          * @param   integer $iWidth     Image width (depends on stream)
  202.          * @param   integer $iHeight    Image height (depends on stream)
  203.          *
  204.          * @return  integer             The image resource (GDLib) containing the full
  205.          *                              episode image.
  206.          *
  207.          * @uses    Episode::getStream() To retrieve the raw image data. Image data is stored
  208.          *                              stored as color indexes; each byte is simply the color
  209.          *                              color index for that pixel. "0" is assumed to be
  210.          *                              transparent.
  211.          * @uses    Episode::getPalette() To retrieve the palette that is used to color
  212.          *                              the image.
  213.          * @uses    Episode::$aBackground To fill the image with a predetermined background
  214.          *                              color, usually "JCS blue".
  215.          *
  216.          * @access  private
  217.          */
  218.         private function getImage($iStream, $iWidth, $iHeight) {
  219.                 $sPixelMap = $this->getStream($iStream);
  220.                 $iImage = imagecreatetruecolor($iWidth, $iHeight);
  221.                 $aPalette = $this->getPalette();
  222.                 imagefill($iImage, 0, 0, imagecolorallocate($iImage, $this->aBackground[0], $this->aBackground[1], $this->aBackground[2]));
  223.  
  224.                 for($y = 0; $y < $iHeight; $y++) {
  225.                         $iRow = $y * $iWidth;
  226.                         for($x = 0; $x < $iWidth; $x++) {
  227.                                 $iByte = $iRow + $x;
  228.                                 list(,$iColor) = unpack("C", substr($sPixelMap, $iByte, 1));
  229.                                 if($iColor > 0) {
  230.                                         $aColor = $aPalette[$iColor];
  231.                                         imagesetpixel($iImage, $x, $y, imagecolorallocate($iImage, $aColor[0], $aColor[1], $aColor[2]));
  232.                                 }
  233.                         }
  234.                 }
  235.  
  236.                 return $iImage;
  237.         }
  238.  
  239.         /**
  240.          * Generate episode illustration image
  241.          *
  242.          * Renders the episode illustration (large image shown on the right side of Jazz
  243.          * Jackrabbit 2 episode menu), stored in data stream one.
  244.          *
  245.          * @return  integer             GD image resource containing the rendered image.
  246.          *
  247.          * @uses    Episode::getHeader() To parse the file header, if needed.
  248.          * @uses    Episode::$aHeader   To retrieve the size of the image to render,
  249.          *                              which is then passed on to the image rendering
  250.          *                              method so it knows the dimensions in which to
  251.          *                              fit the pixels in the data stream.
  252.          * @uses    Episode::getImage() To render the stream
  253.          *
  254.          * @access  public
  255.          */
  256.         public function getIllustration() {
  257.                 if(empty($this->aHeader)) {
  258.                         $this->getHeader();
  259.                 }
  260.                
  261.                 return $this->getImage(1, $this->aHeader['width'], $this->aHeader['height']);
  262.         }
  263.  
  264.         /**
  265.          * Generate highlighted episode title image
  266.          *
  267.          * Renders the highlighted title image (highlighted version of the image shown
  268.          * in the episode selection list), stored in data stream two.
  269.          *
  270.          * @return  integer             GD image resource containing the rendered image.
  271.          *
  272.          * @uses    Episode::getHeader() To parse the file header, if needed.
  273.          * @uses    Episode::$aHeader   To retrieve the size of the image to render,
  274.          *                              which is then passed on to the image rendering
  275.          *                              method so it knows the dimensions in which to
  276.          *                              fit the pixels in the data stream.
  277.          * @uses    Episode::getImage() To render the stream
  278.          *
  279.          * @access  public
  280.          */
  281.         public function getTitleLight() {
  282.                 if(empty($this->aHeader)) {
  283.                         $this->getHeader();
  284.                 }
  285.                
  286.                 return $this->getImage(2, $this->aHeader['titlewidth'], $this->aHeader['titleheight']);
  287.         }
  288.  
  289.         /**
  290.          * Generate episode title image
  291.          *
  292.          * Renders the dark title image (non-highlighted version of the image shown in
  293.          * the episode selection list), stored in data stream three.
  294.          *
  295.          * @return  integer             GD image resource containing the rendered image.
  296.          *
  297.          * @uses    Episode::getHeader() To parse the file header, if needed.
  298.          * @uses    Episode::$aHeader   To retrieve the size of the image to render,
  299.          *                              which is then passed on to the image rendering
  300.          *                              method so it knows the dimensions in which to
  301.          *                              fit the pixels in the data stream.
  302.          * @uses    Episode::getImage() To render the stream
  303.          *
  304.          * @access  public
  305.          */
  306.         public function getTitleDark() {
  307.                 if(empty($this->aHeader)) {
  308.                         $this->getHeader();
  309.                 }
  310.                
  311.                 return $this->getImage(3, $this->aHeader['titlewidth'], $this->aHeader['titleheight']);
  312.         }
  313.        
  314.         /**
  315.          * Get episode palette
  316.          *
  317.          * The palette is generated from a palette file (JASC format) and stored in an
  318.          * array. For results that look good, use the palette Jazz Jackrabbit 2 uses
  319.          * in its menu.
  320.          *
  321.          * @returns array               The palette array. Each color is stored as an
  322.          *                              array(r, g, b) with r, g and b between 0 and 256.
  323.          *
  324.          * @uses    Episode::$aPalette  To store the palette array.
  325.          * @uses    Episode::PALETTE_FILE To read from the palette file.
  326.          *
  327.          * @access  public
  328.          */
  329.         public function getPalette() {
  330.                 if(empty($this->aPalette)) {
  331.                         $aPalette = file(self::PALETTE_FILE);
  332.                         for($i = 3; $i < 259; $i++) {
  333.                                 $this->aPalette[] = explode(' ', trim($aPalette[$i]));
  334.                         }
  335.                 }
  336.                
  337.                 return $this->aPalette;
  338.         }
  339. }