* @version $Revision: 473 $ * @date $Date: 2006-11-24 00:12:21 +0100 (Fri, 24 Nov 2006) $ * @license http://www.gnu.org/licenses/gpl.html GNU General Public License (GPL) * @package PEL */ /**#@+ Required class definitions. */ require_once('PelJpegComment.php'); require_once('PelJpegContent.php'); require_once('PelDataWindow.php'); require_once('PelJpegMarker.php'); require_once('PelException.php'); require_once('PelExif.php'); require_once('Pel.php'); /**#@-*/ /** * Exception thrown when an invalid marker is found. * * This exception is thrown when PEL expects to find a {@link * PelJpegMarker} and instead finds a byte that isn't a known marker. * * @author Martin Geisler * @package PEL * @subpackage Exception */ class PelJpegInvalidMarkerException extends PelException { /** * Construct a new invalid marker exception. * * The exception will contain a message describing the error, * including the byte found and the offset of the offending byte. * * @param int the byte found. * * @param int the offset in the data. */ function __construct($marker, $offset) { parent::__construct('Invalid marker found at offset %d: 0x%2X', $offset, $marker); } } /** * Class for handling JPEG data. * * The {@link PelJpeg} class defined here provides an abstraction for * dealing with a JPEG file. The file will be contain a number of * sections containing some {@link PelJpegContent content} identified * by a {@link PelJpegMarker marker}. * * The {@link getExif()} method is used get hold of the {@link * PelJpegMarker::APP1 APP1} section which stores Exif data. So if * the name of the JPEG file is stored in $filename, then one would * get hold of the Exif data by saying: * * * $jpeg = new PelJpeg($filename); * $exif = $jpeg->getExif(); * $tiff = $exif->getTiff(); * $ifd0 = $tiff->getIfd(); * $exif = $ifd0->getSubIfd(PelIfd::EXIF); * $ifd1 = $ifd0->getNextIfd(); * * * The $idf0 and $ifd1 variables will then be two {@link PelTiff TIFF} * {@link PelIfd Image File Directories}, in which the data is stored * under the keys found in {@link PelTag}. * * Should one have some image data (in the form of a {@link * PelDataWindow}) of an unknown type, then the {@link * PelJpeg::isValid()} function is handy: it will quickly test if the * data could be valid JPEG data. The {@link PelTiff::isValid()} * function does the same for TIFF images. * * @author Martin Geisler * @package PEL */ class PelJpeg { /** * The sections in the JPEG data. * * A JPEG file is built up as a sequence of sections, each section * is identified with a {@link PelJpegMarker}. Some sections can * occur more than once in the JPEG stream (the {@link * PelJpegMarker::DQT DQT} and {@link PelJpegMarker::DHT DTH} * markers for example) and so this is an array of ({@link * PelJpegMarker}, {@link PelJpegContent}) pairs. * * The content can be either generic {@link PelJpegContent JPEG * content} or {@link PelExif Exif data}. * * @var array */ private $sections = array(); /** * The JPEG image data. * * @var PelDataWindow */ private $jpeg_data = null; /** * Construct a new JPEG object. * * The new object will be empty unless an argument is given from * which it can initialize itself. This can either be the filename * of a JPEG image, a {@link PelDataWindow} object or a PHP image * resource handle. * * New Exif data (in the form of a {@link PelExif} object) can be * inserted with the {@link setExif()} method: * * * $jpeg = new PelJpeg($data); * // Create container for the Exif information: * $exif = new PelExif(); * // Now Add a PelTiff object with a PelIfd object with one or more * // PelEntry objects to $exif... Finally add $exif to $jpeg: * $jpeg->setExif($exif); * */ function __construct($data = false) { if ($data === false) return; if (is_string($data)) { Pel::debug('Initializing PelJpeg object from %s', $data); $this->loadFile($data); } elseif ($data instanceof PelDataWindow) { Pel::debug('Initializing PelJpeg object from PelDataWindow.'); $this->load($data); } elseif (is_resource($data) && get_resource_type($data) == 'gd') { Pel::debug('Initializing PelJpeg object from image resource.'); /* 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); $bytes = ob_get_clean(); $this->load(new PelDataWindow($bytes)); } else { throw new PelInvalidArgumentException('Bad type for $data: %s', gettype($data)); } } /** * Load data into a JPEG object. * * The data supplied will be parsed and turned into an object * structure representing the image. This structure can then be * manipulated and later turned back into an string of bytes. * * This methods can be called at any time after a JPEG object has * been constructed, also after the {@link appendSection()} has been * called to append custom sections. Loading several JPEG images * into one object will accumulate the sections, but there will only * be one {@link PelJpegMarker::SOS} section at any given time. * * @param PelDataWindow the data that will be turned into JPEG * sections. */ function load(PelDataWindow $d) { Pel::debug('Parsing %d bytes...', $d->getSize()); /* JPEG data is stored in big-endian format. */ $d->setByteOrder(PelConvert::BIG_ENDIAN); /* Run through the data to read the sections in the image. After * each section is read, the start of the data window will be * moved forward, and after the last section we'll terminate with * no data left in the window. */ while ($d->getSize() > 0) { /* JPEG sections start with 0xFF. The first byte that is not * 0xFF is a marker (hopefully). */ for ($i = 0; $i < 7; $i++) if ($d->getByte($i) != 0xFF) break; $marker = $d->getByte($i); if (!PelJpegMarker::isValid($marker)) throw new PelJpegInvalidMarkerException($marker, $i); /* Move window so first byte becomes first byte in this * section. */ $d->setWindowStart($i+1); if ($marker == PelJpegMarker::SOI || $marker == PelJpegMarker::EOI) { $content = new PelJpegContent(new PelDataWindow()); $this->appendSection($marker, $content); } else { /* Read the length of the section. The length includes the * two bytes used to store the length. */ $len = $d->getShort(0) - 2; Pel::debug('Found %s section of length %d', PelJpegMarker::getName($marker), $len); /* Skip past the length. */ $d->setWindowStart(2); if ($marker == PelJpegMarker::APP1) { try { $content = new PelExif(); $content->load($d->getClone(0, $len)); } catch (PelInvalidDataException $e) { /* We store the data as normal JPEG content if it could * not be parsed as Exif data. */ $content = new PelJpegContent($d->getClone(0, $len)); } $this->appendSection($marker, $content); /* Skip past the data. */ $d->setWindowStart($len); } elseif ($marker == PelJpegMarker::COM) { $content = new PelJpegComment(); $content->load($d->getClone(0, $len)); $this->appendSection($marker, $content); $d->setWindowStart($len); } else { $content = new PelJpegContent($d->getClone(0, $len)); $this->appendSection($marker, $content); /* Skip past the data. */ $d->setWindowStart($len); /* In case of SOS, image data will follow. */ if ($marker == PelJpegMarker::SOS) { /* Some images have some trailing (garbage?) following the * EOI marker. To handle this we seek backwards until we * find the EOI marker. Any trailing content is stored as * a PelJpegContent object. */ $length = $d->getSize(); while ($d->getByte($length-2) != 0xFF || $d->getByte($length-1) != PelJpegMarker::EOI) { $length--; } $this->jpeg_data = $d->getClone(0, $length-2); Pel::debug('JPEG data: ' . $this->jpeg_data->__toString()); /* Append the EOI. */ $this->appendSection(PelJpegMarker::EOI, new PelJpegContent(new PelDataWindow())); /* Now check to see if there are any trailing data. */ if ($length != $d->getSize()) { Pel::maybeThrow(new PelException('Found trailing content ' . 'after EOI: %d bytes', $d->getSize() - $length)); $content = new PelJpegContent($d->getClone($length)); /* We don't have a proper JPEG marker for trailing * garbage, so we just use 0x00... */ $this->appendSection(0x00, $content); } /* Done with the loop. */ break; } } } } /* while ($d->getSize() > 0) */ } /** * Load data from a file into a JPEG object. * * @param string the filename. This must be a readable file. */ function loadFile($filename) { $this->load(new PelDataWindow(file_get_contents($filename))); } /** * Set Exif data. * * Use this to set the Exif data in the image. This will overwrite * any old Exif information in the image. * * @param PelExif the Exif data. */ function setExif(PelExif $exif) { $app0_offset = 1; $app1_offset = -1; /* Search through all sections looking for APP0 or APP1. */ for ($i = 0; $i < count($this->sections); $i++) { if ($this->sections[$i][0] == PelJpegMarker::APP0) { $app0_offset = $i; } elseif ($this->sections[$i][0] == PelJpegMarker::APP1) { $app1_offset = $i; break; } } /* Store the Exif data at the appropriate place, either where the * old Exif data was stored ($app1_offset) or right after APP0 * ($app0_offset+1). */ if ($app1_offset > 0) $this->sections[$app1_offset][1] = $exif; else $this->insertSection(PelJpegMarker::APP1, $exif, $app0_offset+1); } /** * Get Exif data. * * Use this to get the @{link PelExif Exif data} stored. * * @return PelExif the Exif data found or null if the image has no * Exif data. */ function getExif() { $exif = $this->getSection(PelJpegMarker::APP1); if ($exif instanceof PelExif) return $exif; else return null; } /** * Clear any Exif data. * * This method will only clear the first @{link PelJpegMarker::APP1} * section found (there should normally be just one). */ function clearExif() { for ($i = 0; $i < count($this->sections); $i++) { if ($this->sections[$i][0] == PelJpegMarker::APP1) { unset($this->sections[$i]); return; } } } /** * Append a new section. * * Used only when loading an image. If it used again later, then the * section will end up after the @{link PelJpegMarker::EOI EOI * marker} and will probably not be useful. * * Please use @{link setExif()} instead if you intend to add Exif * information to an image as that function will know the right * place to insert the data. * * @param PelJpegMarker the marker identifying the new section. * * @param PelJpegContent the content of the new section. */ function appendSection($marker, PelJpegContent $content) { $this->sections[] = array($marker, $content); } /** * Insert a new section. * * Please use @{link setExif()} instead if you intend to add Exif * information to an image as that function will know the right * place to insert the data. * * @param PelJpegMarker the marker for the new section. * * @param PelJpegContent the content of the new section. * * @param int the offset where the new section will be inserted --- * use 0 to insert it at the very beginning, use 1 to insert it * between sections 1 and 2, etc. */ function insertSection($marker, PelJpegContent $content, $offset) { array_splice($this->sections, $offset, 0, array(array($marker, $content))); } /** * Get a section corresponding to a particular marker. * * Please use the {@link getExif()} if you just need the Exif data. * * This will search through the sections of this JPEG object, * looking for a section identified with the specified {@link * PelJpegMarker marker}. The {@link PelJpegContent content} will * then be returned. The optional argument can be used to skip over * some of the sections. So if one is looking for the, say, third * {@link PelJpegMarker::DHT DHT} section one would do: * * * $dht3 = $jpeg->getSection(PelJpegMarker::DHT, 2); * * * @param PelJpegMarker the marker identifying the section. * * @param int the number of sections to be skipped. This must be a * non-negative integer. * * @return PelJpegContent the content found, or null if there is no * content available. */ function getSection($marker, $skip = 0) { foreach ($this->sections as $s) { if ($s[0] == $marker) if ($skip > 0) $skip--; else return $s[1]; } return null; } /** * Get all sections. * * @return array an array of ({@link PelJpegMarker}, {@link * PelJpegContent}) pairs. Each pair is an array with the {@link * PelJpegMarker} as the first element and the {@link * PelJpegContent} as the second element, so the return type is an * array of arrays. * * So to loop through all the sections in a given JPEG image do * this: * * * foreach ($jpeg->getSections() as $section) { * $marker = $section[0]; * $content = $section[1]; * // Use $marker and $content here. * } * * * instead of this: * * * foreach ($jpeg->getSections() as $marker => $content) { * // Does not work the way you would think... * } * * * The problem is that there could be several sections with the same * marker, and thus a simple associative array does not suffice. */ function getSections() { return $this->sections; } /** * Turn this JPEG object into bytes. * * The bytes returned by this method is ready to be stored in a file * as a valid JPEG image. * * @return string bytes representing this JPEG object, including all * its sections and their associated data. */ function getBytes() { $bytes = ''; foreach ($this->sections as $section) { $m = $section[0]; $c = $section[1]; /* Write the marker */ $bytes .= "\xFF" . PelJpegMarker::getBytes($m); /* Skip over empty markers. */ if ($m == PelJpegMarker::SOI || $m == PelJpegMarker::EOI) continue; $data = $c->getBytes(); $size = strlen($data) + 2; $bytes .= PelConvert::shortToBytes($size, PelConvert::BIG_ENDIAN); $bytes .= $data; /* In case of SOS, we need to write the JPEG data. */ if ($m == PelJpegMarker::SOS) $bytes .= $this->jpeg_data->getBytes(); } return $bytes; } /** * Make a string representation of this JPEG object. * * This is mainly usefull for debugging. It will show the structure * of the image, and its sections. * * @return string debugging information about this JPEG object. */ function __toString() { $str = Pel::tra("Dumping JPEG data...\n"); for ($i = 0; $i < count($this->sections); $i++) { $m = $this->sections[$i][0]; $c = $this->sections[$i][1]; $str .= Pel::fmt("Section %d (marker 0x%02X - %s):\n", $i, $m, PelJpegMarker::getName($m)); $str .= Pel::fmt(" Description: %s\n", PelJpegMarker::getDescription($m)); if ($m == PelJpegMarker::SOI || $m == PelJpegMarker::EOI) continue; if ($c instanceof PelExif) { $str .= Pel::tra(" Content : Exif data\n"); $str .= $c->__toString() . "\n"; } elseif ($c instanceof PelJpegComment) { $str .= Pel::fmt(" Content : %s\n", $c->getValue()); } else { $str .= Pel::tra(" Content : Unknown\n"); } } return $str; } /** * Test data to see if it could be a valid JPEG image. * * The function will only look at the first few bytes of the data, * and try to determine if it could be a valid JPEG image based on * those bytes. This means that the check is more like a heuristic * than a rigorous check. * * @param PelDataWindow the bytes that will be checked. * * @return boolean true if the bytes look like the beginning of a * JPEG image, false otherwise. * * @see PelTiff::isValid() */ static function isValid(PelDataWindow $d) { /* JPEG data is stored in big-endian format. */ $d->setByteOrder(PelConvert::BIG_ENDIAN); for ($i = 0; $i < 7; $i++) if ($d->getByte($i) != 0xFF) break; return $d->getByte($i) == PelJpegMarker::SOI; } } ?>