<?php

/*  PEL: PHP Exif Library.  A library with support for reading and
 *  writing all Exif headers in JPEG and TIFF images using PHP.
 *
 *  Copyright (C) 2004, 2005, 2006, 2007  Martin Geisler.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program in the file COPYING; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 *  Boston, MA 02110-1301 USA
 */

/* $Id$ */


/**
 * A container for bytes with a limited window of accessible bytes.
 *
 * @author Martin Geisler <mgeisler@users.sourceforge.net>
 * @version $Revision$
 * @date $Date$
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License (GPL)
 * @package PEL
 */

/**#@+ Required class definitions. */
require_once('PelException.php');
require_once('PelConvert.php');
/**#@-*/


/**
 * An exception thrown when an invalid offset is encountered.
 *
 * @package PEL
 * @subpackage Exception
 */
class PelDataWindowOffsetException extends PelException {}

/**
 * An exception thrown when an invalid window is encountered.
 *
 * @package PEL
 * @subpackage Exception
 */
class PelDataWindowWindowException extends PelException {}

/**
 * The window.
 *
 * @package PEL
 */
class PelDataWindow {

  /**
   * The data held by this window.
   *
   * The string can contain any kind of data, including binary data.
   *
   * @var string
   */
  private $data = '';

  /**
   * The byte order currently in use.
   *
   * This will be the byte order used when data is read using the for
   * example the {@link getShort} function.  It must be one of {@link
   * PelConvert::LITTLE_ENDIAN} and {@link PelConvert::BIG_ENDIAN}.
   *
   * @var PelByteOrder
   * @see setByteOrder, getByteOrder
   */
  private $order;

  /**
   * The start of the current window.
   *
   * All offsets used for access into the data will count from this
   * offset, effectively limiting access to a window starting at this
   * byte.
   *
   * @var int
   * @see setWindowStart
   */
  private $start  = 0;

  /**
   * The size of the current window.
   *
   * All offsets used for access into the data will be limited by this
   * variable.  A valid offset must be strictly less than this
   * variable.
   *
   * @var int
   * @see setWindowSize
   */
  private $size   = 0;


  /**
   * Construct a new data window with the data supplied.
   *
   * @param mixed the data that this window will contain. This can
   * either be given as a string (interpreted litteraly as a sequence
   * of bytes) or a PHP image resource handle. The data will be copied
   * into the new data window.
   *
   * @param boolean the initial byte order of the window.  This must
   * be either {@link PelConvert::LITTLE_ENDIAN} or {@link
   * PelConvert::BIG_ENDIAN}.  This will be used when integers are
   * read from the data, and it can be changed later with {@link
   * setByteOrder()}.
   */
  function __construct($data = '', $endianess = PelConvert::LITTLE_ENDIAN) {
    if (is_string($data)) {
      $this->data = $data;
    } elseif (is_resource($data) && get_resource_type($data) == 'gd') {
      /* The ImageJpeg() function insists on printing the bytes
       * instead of returning them in a more civil way as a string, so
       * we have to buffer the output... */
      ob_start();
      ImageJpeg($data, null, Pel::$quality);
      $this->data = ob_get_clean();
    } else {
      throw new PelInvalidArgumentException('Bad type for $data: %s', 
                                            gettype($data));
    }

    $this->order = $endianess;
    $this->size  = strlen($data);
  }


  /**
   * Get the size of the data window.
   *
   * @return int the number of bytes covered by the window.  The
   * allowed offsets go from 0 up to this number minus one.
   *
   * @see getBytes()
   */
  function getSize() {
    return $this->size;
  }


  /**
   * Change the byte order of the data.
   *
   * @param PelByteOrder the new byte order.  This must be either
   * {@link PelConvert::LITTLE_ENDIAN} or {@link
   * PelConvert::BIG_ENDIAN}.
   */
  function setByteOrder($o) {
    $this->order = $o;
  }


  /**
   * Get the currently used byte order.
   *
   * @return PelByteOrder this will be either {@link
   * PelConvert::LITTLE_ENDIAN} or {@link PelConvert::BIG_ENDIAN}.
   */
  function getByteOrder() {
    return $this->order;
  }


  /* Move the start of the window forward.
   *
   * @param int the new start of the window.  All new offsets will be
   * calculated from this new start offset, and the size of the window
   * will shrink to keep the end of the window in place.
   */
  function setWindowStart($start) {
    if ($start < 0 || $start > $this->size)
      throw new PelDataWindowWindowException('Window [%d, %d] does ' .
                                             'not fit in window [0, %d]',
                                             $start, $this->size, $this->size);

    $this->start += $start;
    $this->size  -= $start;
  }


  /**
   * Adjust the size of the window.
   *
   * The size can only be made smaller.
   *
   * @param int the desired size of the window.  If the argument is
   * negative, the window will be shrunk by the argument.
   */
  function setWindowSize($size) {
    if ($size < 0)
      $size += $this->size;

    if ($size < 0 || $size > $this->size)
      throw new PelDataWindowWindowException('Window [0, %d] ' .
                                             'does not fit in window [0, %d]',
                                             $size, $this->size);
    $this->size = $size;
  }
  

  /**
   * Make a new data window with the same data as the this window.
   *
   * @param mixed if an integer is supplied, then it will be the start
   * of the window in the clone.  If left unspecified, then the clone
   * will inherit the start from this object.
   *
   * @param mixed if an integer is supplied, then it will be the size
   * of the window in the clone.  If left unspecified, then the clone
   * will inherit the size from this object.
   *
   * @return PelDataWindow a new window that operates on the same data
   * as this window, but (optionally) with a smaller window size.
   */
  function getClone($start = false, $size = false) {
    $c = clone $this;
    
    if (is_int($start))
      $c->setWindowStart($start);

    if (is_int($size))
      $c->setWindowSize($size);

    return $c;
  }


  /**
   * Validate an offset against the current window.
   *
   * @param int the offset to be validated.  If the offset is negative
   * or if it is greater than or equal to the current window size,
   * then a {@link PelDataWindowOffsetException} is thrown.
   *
   * @return void if the offset is valid nothing is returned, if it is
   * invalid a new {@link PelDataWindowOffsetException} is thrown.
   */
  private function validateOffset($o) {
    if ($o < 0 || $o >= $this->size)
      throw new PelDataWindowOffsetException('Offset %d not within [%d, %d]',
                                             $o, 0, $this->size-1);
  }


  /**
   * Return some or all bytes visible in the window.
   *
   * This method works just like the standard {@link substr()}
   * function in PHP with the exception that it works within the
   * window of accessible bytes and does strict range checking.
   * 
   * @param int the offset to the first byte returned.  If a negative
   * number is given, then the counting will be from the end of the
   * window.  Invalid offsets will result in a {@link
   * PelDataWindowOffsetException} being thrown.
   *
   * @param int the size of the sub-window.  If a negative number is
   * given, then that many bytes will be omitted from the result.
   *
   * @return string a subset of the bytes in the window.  This will
   * always return no more than {@link getSize()} bytes.
   */
  function getBytes($start = false, $size = false) {
    if (is_int($start)) {
      if ($start < 0)
        $start += $this->size;
      
      $this->validateOffset($start);
    } else {
      $start = 0;
    }
    
    if (is_int($size)) {
      if ($size <= 0)
        $size += $this->size - $start;
      
      $this->validateOffset($start+$size);
    } else {
      $size = $this->size - $start;
    }

    return substr($this->data, $this->start + $start, $size);
  }


  /**
   * Return an unsigned byte from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first byte in the current allowed window.  The last
   * valid offset is equal to {@link getSize()}-1.  Invalid offsets
   * will result in a {@link PelDataWindowOffsetException} being
   * thrown.
   *
   * @return  int  the unsigned byte found at offset.
   */
  function getByte($o = 0) {
    /* Validate the offset --- this throws an exception if offset is
     * out of range. */
    $this->validateOffset($o);

    /* Translate the offset into an offset into the data. */
    $o += $this->start;
    
    /* Return an unsigned byte. */
    return PelConvert::bytesToByte($this->data, $o);
  }


  /**
   * Return a signed byte from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first byte in the current allowed window.  The last
   * valid offset is equal to {@link getSize()}-1.  Invalid offsets
   * will result in a {@link PelDataWindowOffsetException} being
   * thrown.
   *
   * @return  int  the signed byte found at offset.
   */
  function getSByte($o = 0) {
    /* Validate the offset --- this throws an exception if offset is
     * out of range. */
    $this->validateOffset($o);

    /* Translate the offset into an offset into the data. */
    $o += $this->start;
    
    /* Return a signed byte. */
    return PelConvert::bytesToSByte($this->data, $o);
  }


  /**
   * Return an unsigned short read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first short available in the current allowed window.
   * The last valid offset is equal to {@link getSize()}-2.  Invalid
   * offsets will result in a {@link PelDataWindowOffsetException}
   * being thrown.
   *
   * @return  int  the unsigned short found at offset.
   */
  function getShort($o = 0) {
    /* Validate the offset+1 to see if we can safely get two bytes ---
     * this throws an exception if offset is out of range. */
    $this->validateOffset($o);
    $this->validateOffset($o+1);

    /* Translate the offset into an offset into the data. */
    $o += $this->start;

    /* Return an unsigned short. */
    return PelConvert::bytesToShort($this->data, $o, $this->order);
  }


  /**
   * Return a signed short read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first short available in the current allowed window.
   * The last valid offset is equal to {@link getSize()}-2.  Invalid
   * offsets will result in a {@link PelDataWindowOffsetException}
   * being thrown.
   *
   * @return  int  the signed short found at offset.
   */
  function getSShort($o = 0) {
    /* Validate the offset+1 to see if we can safely get two bytes ---
     * this throws an exception if offset is out of range. */
    $this->validateOffset($o);
    $this->validateOffset($o+1);

    /* Translate the offset into an offset into the data. */
    $o += $this->start;

    /* Return a signed short. */
    return PelConvert::bytesToSShort($this->data, $o, $this->order);
  }


  /**
   * Return an unsigned long read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first long available in the current allowed window.
   * The last valid offset is equal to {@link getSize()}-4.  Invalid
   * offsets will result in a {@link PelDataWindowOffsetException}
   * being thrown.
   *
   * @return  int  the unsigned long found at offset.
   */
  function getLong($o = 0) {
    /* Validate the offset+3 to see if we can safely get four bytes
     * --- this throws an exception if offset is out of range. */
    $this->validateOffset($o);
    $this->validateOffset($o+3);
   
    /* Translate the offset into an offset into the data. */
    $o += $this->start;

    /* Return an unsigned long. */
    return PelConvert::bytesToLong($this->data, $o, $this->order);
  }


  /**
   * Return a signed long read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first long available in the current allowed window.
   * The last valid offset is equal to {@link getSize()}-4.  Invalid
   * offsets will result in a {@link PelDataWindowOffsetException}
   * being thrown.
   *
   * @return  int  the signed long found at offset.
   */
  function getSLong($o = 0) {
    /* Validate the offset+3 to see if we can safely get four bytes
     * --- this throws an exception if offset is out of range. */
    $this->validateOffset($o);
    $this->validateOffset($o+3);
   
    /* Translate the offset into an offset into the data. */
    $o += $this->start;

    /* Return a signed long. */
    return PelConvert::bytesToSLong($this->data, $o, $this->order);
  }


  /**
   * Return an unsigned rational read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first rational available in the current allowed
   * window.  The last valid offset is equal to {@link getSize()}-8.
   * Invalid offsets will result in a {@link
   * PelDataWindowOffsetException} being thrown.
   *
   * @return array the unsigned rational found at offset.  A rational
   * number is represented as an array of two numbers: the enumerator
   * and denominator.  Both of these numbers will be unsigned longs.
   */
  function getRational($o = 0) {
    return array($this->getLong($o), $this->getLong($o+4));
  }


  /**
   * Return a signed rational read from the data.
   *
   * @param int the offset into the data.  An offset of zero will
   * return the first rational available in the current allowed
   * window.  The last valid offset is equal to {@link getSize()}-8.
   * Invalid offsets will result in a {@link
   * PelDataWindowOffsetException} being thrown.
   *
   * @return array the signed rational found at offset.  A rational
   * number is represented as an array of two numbers: the enumerator
   * and denominator.  Both of these numbers will be signed longs.
   */
  function getSRational($o = 0) {
    return array($this->getSLong($o), $this->getSLong($o+4));
  }


  /**
   * String comparison on substrings.
   *
   * @param int the offset into the data.  An offset of zero will make
   * the comparison start with the very first byte available in the
   * window.  The last valid offset is equal to {@link getSize()}
   * minus the length of the string.  If the string is too long, then
   * a {@link PelDataWindowOffsetException} will be thrown.
   *
   * @param string the string to compare with.
   *
   * @return boolean true if the string given matches the data in the
   * window, at the specified offset, false otherwise.  The comparison
   * will stop as soon as a mismatch if found.
   */
  function strcmp($o, $str) {
    /* Validate the offset of the final character we might have to
     * check. */
    $s = strlen($str);
    $this->validateOffset($o);
    $this->validateOffset($o + $s - 1);

    /* Translate the offset into an offset into the data. */
    $o += $this->start;
  
    /* Check each character, return as soon as the answer is known. */
    for ($i = 0; $i < $s; $i++) {
      if ($this->data{$o + $i} != $str{$i})
        return false;
    }

    /* All characters matches each other, return true. */
    return true;
  }


  /**
   * Return a string representation of the data window.
   *
   * @return string a description of the window with information about
   * the number of bytes accessible, the total number of bytes, and
   * the window start and stop.
   */
  function __toString() {
    return Pel::fmt('DataWindow: %d bytes in [%d, %d] of %d bytes',
                    $this->size,
                    $this->start, $this->start + $this->size,
                    strlen($this->data));
  }

}

?>