1200 lines
36 KiB
PHP
1200 lines
36 KiB
PHP
<?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 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: PelIfd.php 443 2006-09-17 18:32:04Z mgeisler $ */
|
|
|
|
|
|
/**
|
|
* Classes for dealing with Exif IFDs.
|
|
*
|
|
* @author Martin Geisler <mgeisler@users.sourceforge.net>
|
|
* @version $Revision: 443 $
|
|
* @date $Date: 2006-09-17 20:32:04 +0200 (Sun, 17 Sep 2006) $
|
|
* @license http://www.gnu.org/licenses/gpl.html GNU General Public
|
|
* License (GPL)
|
|
* @package PEL
|
|
*/
|
|
|
|
/**#@+ Required class definitions. */
|
|
require_once('PelEntryUndefined.php');
|
|
require_once('PelEntryRational.php');
|
|
require_once('PelDataWindow.php');
|
|
require_once('PelEntryAscii.php');
|
|
require_once('PelEntryShort.php');
|
|
require_once('PelEntryByte.php');
|
|
require_once('PelEntryLong.php');
|
|
require_once('PelException.php');
|
|
require_once('PelFormat.php');
|
|
require_once('PelEntry.php');
|
|
require_once('PelTag.php');
|
|
require_once('Pel.php');
|
|
/**#@-*/
|
|
|
|
|
|
/**
|
|
* Exception indicating a general problem with the IFD.
|
|
*
|
|
* @author Martin Geisler <mgeisler@users.sourceforge.net>
|
|
* @package PEL
|
|
* @subpackage Exception
|
|
*/
|
|
class PelIfdException extends PelException {}
|
|
|
|
/**
|
|
* Class representing an Image File Directory (IFD).
|
|
*
|
|
* {@link PelTiff TIFF data} is structured as a number of Image File
|
|
* Directories, IFDs for short. Each IFD contains a number of {@link
|
|
* PelEntry entries}, some data and finally a link to the next IFD.
|
|
*
|
|
* @author Martin Geisler <mgeisler@users.sourceforge.net>
|
|
* @package PEL
|
|
*/
|
|
class PelIfd implements IteratorAggregate, ArrayAccess {
|
|
|
|
/**
|
|
* Main image IFD.
|
|
*
|
|
* Pass this to the constructor when creating an IFD which will be
|
|
* the IFD of the main image.
|
|
*/
|
|
const IFD0 = 0;
|
|
|
|
/**
|
|
* Thumbnail image IFD.
|
|
*
|
|
* Pass this to the constructor when creating an IFD which will be
|
|
* the IFD of the thumbnail image.
|
|
*/
|
|
const IFD1 = 1;
|
|
|
|
/**
|
|
* Exif IFD.
|
|
*
|
|
* Pass this to the constructor when creating an IFD which will be
|
|
* the Exif sub-IFD.
|
|
*/
|
|
const EXIF = 2;
|
|
|
|
/**
|
|
* GPS IFD.
|
|
*
|
|
* Pass this to the constructor when creating an IFD which will be
|
|
* the GPS sub-IFD.
|
|
*/
|
|
const GPS = 3;
|
|
|
|
/**
|
|
* Interoperability IFD.
|
|
*
|
|
* Pass this to the constructor when creating an IFD which will be
|
|
* the interoperability sub-IFD.
|
|
*/
|
|
const INTEROPERABILITY = 4;
|
|
|
|
/**
|
|
* The entries held by this directory.
|
|
*
|
|
* Each tag in the directory is represented by a {@link PelEntry}
|
|
* object in this array.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $entries = array();
|
|
|
|
/**
|
|
* The type of this directory.
|
|
*
|
|
* Initialized in the constructor. Must be one of {@link IFD0},
|
|
* {@link IFD1}, {@link EXIF}, {@link GPS}, or {@link
|
|
* INTEROPERABILITY}.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $type;
|
|
|
|
/**
|
|
* The next directory.
|
|
*
|
|
* This will be initialized in the constructor, or be left as null
|
|
* if this is the last directory.
|
|
*
|
|
* @var PelIfd
|
|
*/
|
|
private $next = null;
|
|
|
|
/**
|
|
* Sub-directories pointed to by this directory.
|
|
*
|
|
* This will be an array of ({@link PelTag}, {@link PelIfd}) pairs.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $sub = array();
|
|
|
|
/**
|
|
* The thumbnail data.
|
|
*
|
|
* This will be initialized in the constructor, or be left as null
|
|
* if there are no thumbnail as part of this directory.
|
|
*
|
|
* @var PelDataWindow
|
|
*/
|
|
private $thumb_data = null;
|
|
// TODO: use this format to choose between the
|
|
// JPEG_INTERCHANGE_FORMAT and STRIP_OFFSETS tags.
|
|
// private $thumb_format;
|
|
|
|
|
|
/**
|
|
* Construct a new Image File Directory (IFD).
|
|
*
|
|
* The IFD will be empty, use the {@link addEntry()} method to add
|
|
* an {@link PelEntry}. Use the {@link setNext()} method to link
|
|
* this IFD to another.
|
|
*
|
|
* @param int type the type of this IFD. Must be one of {@link
|
|
* IFD0}, {@link IFD1}, {@link EXIF}, {@link GPS}, or {@link
|
|
* INTEROPERABILITY}. An {@link PelIfdException} will be thrown
|
|
* otherwise.
|
|
*/
|
|
function __construct($type) {
|
|
if ($type != PelIfd::IFD0 && $type != PelIfd::IFD1 &&
|
|
$type != PelIfd::EXIF && $type != PelIfd::GPS &&
|
|
$type != PelIfd::INTEROPERABILITY)
|
|
throw new PelIfdException('Unknown IFD type: %d', $type);
|
|
|
|
$this->type = $type;
|
|
}
|
|
|
|
|
|
/**
|
|
* Load data into a Image File Directory (IFD).
|
|
*
|
|
* @param PelDataWindow the data window that will provide the data.
|
|
*
|
|
* @param int the offset within the window where the directory will
|
|
* be found.
|
|
*/
|
|
function load(PelDataWindow $d, $offset) {
|
|
$thumb_offset = 0;
|
|
$thumb_length = 0;
|
|
|
|
Pel::debug('Constructing IFD at offset %d from %d bytes...',
|
|
$offset, $d->getSize());
|
|
|
|
/* Read the number of entries */
|
|
$n = $d->getShort($offset);
|
|
Pel::debug('Loading %d entries...', $n);
|
|
|
|
$offset += 2;
|
|
|
|
/* Check if we have enough data. */
|
|
if ($offset + 12 * $n > $d->getSize()) {
|
|
$n = floor(($offset - $d->getSize()) / 12);
|
|
Pel::maybeThrow(new PelIfdException('Adjusted to: %d.', $n));
|
|
}
|
|
|
|
for ($i = 0; $i < $n; $i++) {
|
|
// TODO: increment window start instead of using offsets.
|
|
$tag = $d->getShort($offset + 12 * $i);
|
|
Pel::debug('Loading entry with tag 0x%04X: %s (%d of %d)...',
|
|
$tag, PelTag::getName($this->type, $tag), $i + 1, $n);
|
|
|
|
switch ($tag) {
|
|
case PelTag::EXIF_IFD_POINTER:
|
|
case PelTag::GPS_INFO_IFD_POINTER:
|
|
case PelTag::INTEROPERABILITY_IFD_POINTER:
|
|
$o = $d->getLong($offset + 12 * $i + 8);
|
|
Pel::debug('Found sub IFD at offset %d', $o);
|
|
|
|
/* Map tag to IFD type. */
|
|
if ($tag == PelTag::EXIF_IFD_POINTER)
|
|
$type = PelIfd::EXIF;
|
|
elseif ($tag == PelTag::GPS_INFO_IFD_POINTER)
|
|
$type = PelIfd::GPS;
|
|
elseif ($tag == PelTag::INTEROPERABILITY_IFD_POINTER)
|
|
$type = PelIfd::INTEROPERABILITY;
|
|
|
|
$this->sub[$type] = new PelIfd($type);
|
|
$this->sub[$type]->load($d, $o);
|
|
break;
|
|
case PelTag::JPEG_INTERCHANGE_FORMAT:
|
|
$thumb_offset = $d->getLong($offset + 12 * $i + 8);
|
|
$this->safeSetThumbnail($d, $thumb_offset, $thumb_length);
|
|
break;
|
|
case PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH:
|
|
$thumb_length = $d->getLong($offset + 12 * $i + 8);
|
|
$this->safeSetThumbnail($d, $thumb_offset, $thumb_length);
|
|
break;
|
|
default:
|
|
$format = $d->getShort($offset + 12 * $i + 2);
|
|
$components = $d->getLong($offset + 12 * $i + 4);
|
|
|
|
/* The data size. If bigger than 4 bytes, the actual data is
|
|
* not in the entry but somewhere else, with the offset stored
|
|
* in the entry.
|
|
*/
|
|
$s = PelFormat::getSize($format) * $components;
|
|
if ($s > 0) {
|
|
$doff = $offset + 12 * $i + 8;
|
|
if ($s > 4)
|
|
$doff = $d->getLong($doff);
|
|
|
|
$data = $d->getClone($doff, $s);
|
|
} else {
|
|
$data = new PelDataWindow();
|
|
}
|
|
|
|
try {
|
|
$entry = $this->newEntryFromData($tag, $format, $components, $data);
|
|
|
|
if ($this->isValidTag($tag)) {
|
|
$entry->setIfdType($this->type);
|
|
$this->entries[$tag] = $entry;
|
|
} else {
|
|
Pel::maybeThrow(new PelInvalidDataException("IFD %s cannot hold\n%s",
|
|
$this->getName(),
|
|
$entry->__toString()));
|
|
}
|
|
} catch (PelException $e) {
|
|
/* Throw the exception when running in strict mode, store
|
|
* otherwise. */
|
|
Pel::maybeThrow($e);
|
|
}
|
|
|
|
/* The format of the thumbnail is stored in this tag. */
|
|
// TODO: handle TIFF thumbnail.
|
|
// if ($tag == PelTag::COMPRESSION) {
|
|
// $this->thumb_format = $data->getShort();
|
|
// }
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Offset to next IFD */
|
|
$o = $d->getLong($offset + 12 * $n);
|
|
Pel::debug('Current offset is %d, link at %d points to %d.',
|
|
$offset, $offset + 12 * $n, $o);
|
|
|
|
if ($o > 0) {
|
|
/* Sanity check: we need 6 bytes */
|
|
if ($o > $d->getSize() - 6) {
|
|
Pel::maybeThrow(new PelIfdException('Bogus offset to next IFD: ' .
|
|
'%d > %d!',
|
|
$o, $d->getSize() - 6));
|
|
} else {
|
|
if ($this->type == PelIfd::IFD1) // IFD1 shouldn't link further...
|
|
Pel::maybeThrow(new PelIfdException('IFD1 links to another IFD!'));
|
|
|
|
$this->next = new PelIfd(PelIfd::IFD1);
|
|
$this->next->load($d, $o);
|
|
}
|
|
} else {
|
|
Pel::debug('Last IFD.');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Make a new entry from a bunch of bytes.
|
|
*
|
|
* This method will create the proper subclass of {@link PelEntry}
|
|
* corresponding to the {@link PelTag} and {@link PelFormat} given.
|
|
* The entry will be initialized with the data given.
|
|
*
|
|
* Please note that the data you pass to this method should come
|
|
* from an image, that is, it should be raw bytes. If instead you
|
|
* want to create an entry for holding, say, an short integer, then
|
|
* create a {@link PelEntryShort} object directly and load the data
|
|
* into it.
|
|
*
|
|
* A {@link PelUnexpectedFormatException} is thrown if a mismatch is
|
|
* discovered between the tag and format, and likewise a {@link
|
|
* PelWrongComponentCountException} is thrown if the number of
|
|
* components does not match the requirements of the tag. The
|
|
* requirements for a given tag (if any) can be found in the
|
|
* documentation for {@link PelTag}.
|
|
*
|
|
* @param PelTag the tag of the entry.
|
|
*
|
|
* @param PelFormat the format of the entry.
|
|
*
|
|
* @param int the components in the entry.
|
|
*
|
|
* @param PelDataWindow the data which will be used to construct the
|
|
* entry.
|
|
*
|
|
* @return PelEntry a newly created entry, holding the data given.
|
|
*/
|
|
function newEntryFromData($tag, $format, $components, PelDataWindow $data) {
|
|
|
|
/* First handle tags for which we have a specific PelEntryXXX
|
|
* class. */
|
|
|
|
switch ($this->type) {
|
|
|
|
case self::IFD0:
|
|
case self::IFD1:
|
|
case self::EXIF:
|
|
case self::INTEROPERABILITY:
|
|
|
|
switch ($tag) {
|
|
case PelTag::DATE_TIME:
|
|
case PelTag::DATE_TIME_ORIGINAL:
|
|
case PelTag::DATE_TIME_DIGITIZED:
|
|
if ($format != PelFormat::ASCII)
|
|
throw new PelUnexpectedFormatException($this->type, $tag, $format,
|
|
PelFormat::ASCII);
|
|
|
|
if ($components != 20)
|
|
throw new PelWrongComponentCountException($this->type, $tag, $components, 20);
|
|
|
|
// TODO: handle timezones.
|
|
return new PelEntryTime($tag, $data->getBytes(0, -1), PelEntryTime::EXIF_STRING);
|
|
|
|
case PelTag::COPYRIGHT:
|
|
if ($format != PelFormat::ASCII)
|
|
throw new PelUnexpectedFormatException($this->type, $tag, $format,
|
|
PelFormat::ASCII);
|
|
|
|
$v = explode("\0", trim($data->getBytes(), ' '));
|
|
return new PelEntryCopyright($v[0], $v[1]);
|
|
|
|
case PelTag::EXIF_VERSION:
|
|
case PelTag::FLASH_PIX_VERSION:
|
|
case PelTag::INTEROPERABILITY_VERSION:
|
|
if ($format != PelFormat::UNDEFINED)
|
|
throw new PelUnexpectedFormatException($this->type, $tag, $format,
|
|
PelFormat::UNDEFINED);
|
|
|
|
return new PelEntryVersion($tag, $data->getBytes() / 100);
|
|
|
|
case PelTag::USER_COMMENT:
|
|
if ($format != PelFormat::UNDEFINED)
|
|
throw new PelUnexpectedFormatException($this->type, $tag, $format,
|
|
PelFormat::UNDEFINED);
|
|
if ($data->getSize() < 8) {
|
|
return new PelEntryUserComment();
|
|
} else {
|
|
return new PelEntryUserComment($data->getBytes(8),
|
|
rtrim($data->getBytes(0, 8)));
|
|
}
|
|
|
|
case PelTag::XP_TITLE:
|
|
case PelTag::XP_COMMENT:
|
|
case PelTag::XP_AUTHOR:
|
|
case PelTag::XP_KEYWORDS:
|
|
case PelTag::XP_SUBJECT:
|
|
if ($format != PelFormat::BYTE)
|
|
throw new PelUnexpectedFormatException($this->type, $tag, $format,
|
|
PelFormat::BYTE);
|
|
|
|
$v = '';
|
|
for ($i = 0; $i < $components; $i++) {
|
|
$b = $data->getByte($i);
|
|
/* Convert the byte to a character if it is non-null ---
|
|
* information about the character encoding of these entries
|
|
* would be very nice to have! So far my tests have shown
|
|
* that characters in the Latin-1 character set are stored in
|
|
* a single byte followed by a NULL byte. */
|
|
if ($b != 0)
|
|
$v .= chr($b);
|
|
}
|
|
|
|
return new PelEntryWindowsString($tag, $v);
|
|
}
|
|
|
|
case self::GPS:
|
|
|
|
default:
|
|
/* Then handle the basic formats. */
|
|
switch ($format) {
|
|
case PelFormat::BYTE:
|
|
$v = new PelEntryByte($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getByte($i));
|
|
return $v;
|
|
|
|
case PelFormat::SBYTE:
|
|
$v = new PelEntrySByte($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getSByte($i));
|
|
return $v;
|
|
|
|
case PelFormat::ASCII:
|
|
return new PelEntryAscii($tag, $data->getBytes(0, -1));
|
|
|
|
case PelFormat::SHORT:
|
|
$v = new PelEntryShort($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getShort($i*2));
|
|
return $v;
|
|
|
|
case PelFormat::SSHORT:
|
|
$v = new PelEntrySShort($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getSShort($i*2));
|
|
return $v;
|
|
|
|
case PelFormat::LONG:
|
|
$v = new PelEntryLong($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getLong($i*4));
|
|
return $v;
|
|
|
|
case PelFormat::SLONG:
|
|
$v = new PelEntrySLong($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getSLong($i*4));
|
|
return $v;
|
|
|
|
case PelFormat::RATIONAL:
|
|
$v = new PelEntryRational($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getRational($i*8));
|
|
return $v;
|
|
|
|
case PelFormat::SRATIONAL:
|
|
$v = new PelEntrySRational($tag);
|
|
for ($i = 0; $i < $components; $i++)
|
|
$v->addNumber($data->getSRational($i*8));
|
|
return $v;
|
|
|
|
case PelFormat::UNDEFINED:
|
|
return new PelEntryUndefined($tag, $data->getBytes());
|
|
|
|
default:
|
|
throw new PelException('Unsupported format: %s',
|
|
PelFormat::getName($format));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Extract thumbnail data safely.
|
|
*
|
|
* It is safe to call this method repeatedly with either the offset
|
|
* or the length set to zero, since it requires both of these
|
|
* arguments to be positive before the thumbnail is extracted.
|
|
*
|
|
* When both parameters are set it will check the length against the
|
|
* available data and adjust as necessary. Only then is the
|
|
* thumbnail data loaded.
|
|
*
|
|
* @param PelDataWindow the data from which the thumbnail will be
|
|
* extracted.
|
|
*
|
|
* @param int the offset into the data.
|
|
*
|
|
* @param int the length of the thumbnail.
|
|
*/
|
|
private function safeSetThumbnail(PelDataWindow $d, $offset, $length) {
|
|
/* Load the thumbnail if both the offset and the length is
|
|
* available. */
|
|
if ($offset > 0 && $length > 0) {
|
|
/* Some images have a broken length, so we try to carefully
|
|
* check the length before we store the thumbnail. */
|
|
if ($offset + $length > $d->getSize()) {
|
|
Pel::maybeThrow(new PelIfdException('Thumbnail length %d bytes ' .
|
|
'adjusted to %d bytes.',
|
|
$length,
|
|
$d->getSize() - $offset));
|
|
$length = $d->getSize() - $offset;
|
|
}
|
|
|
|
/* Now set the thumbnail normally. */
|
|
$this->setThumbnail($d->getClone($offset, $length));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Set thumbnail data.
|
|
*
|
|
* Use this to embed an arbitrary JPEG image within this IFD. The
|
|
* data will be checked to ensure that it has a proper {@link
|
|
* PelJpegMarker::EOI} at the end. If not, then the length is
|
|
* adjusted until one if found. An {@link PelIfdException} might be
|
|
* thrown (depending on {@link Pel::$strict}) this case.
|
|
*
|
|
* @param PelDataWindow the thumbnail data.
|
|
*/
|
|
function setThumbnail(PelDataWindow $d) {
|
|
$size = $d->getSize();
|
|
/* Now move backwards until we find the EOI JPEG marker. */
|
|
while ($d->getByte($size - 2) != 0xFF ||
|
|
$d->getByte($size - 1) != PelJpegMarker::EOI) {
|
|
$size--;
|
|
}
|
|
|
|
if ($size != $d->getSize())
|
|
Pel::maybeThrow(new PelIfdException('Decrementing thumbnail size ' .
|
|
'to %d bytes', $size));
|
|
|
|
$this->thumb_data = $d->getClone(0, $size);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the type of this directory.
|
|
*
|
|
* @return int of {@link PelIfd::IFD0}, {@link PelIfd::IFD1}, {@link
|
|
* PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
|
|
* PelIfd::INTEROPERABILITY}.
|
|
*/
|
|
function getType() {
|
|
return $this->type;
|
|
}
|
|
|
|
|
|
/**
|
|
* Is a given tag valid for this IFD?
|
|
*
|
|
* Different types of IFDs can contain different kinds of tags ---
|
|
* the {@link IFD0} type, for example, cannot contain a {@link
|
|
* PelTag::GPS_LONGITUDE} tag.
|
|
*
|
|
* A special exception is tags with values above 0xF000. They are
|
|
* treated as private tags and will be allowed everywhere (use this
|
|
* for testing or for implementing your own types of tags).
|
|
*
|
|
* @param PelTag the tag.
|
|
*
|
|
* @return boolean true if the tag is considered valid in this IFD,
|
|
* false otherwise.
|
|
*
|
|
* @see getValidTags()
|
|
*/
|
|
function isValidTag($tag) {
|
|
return $tag > 0xF000 || in_array($tag, $this->getValidTags());
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a list of valid tags for this IFD.
|
|
*
|
|
* @return array an array of {@link PelTag}s which are valid for
|
|
* this IFD.
|
|
*/
|
|
function getValidTags() {
|
|
switch ($this->type) {
|
|
case PelIfd::IFD0:
|
|
case PelIfd::IFD1:
|
|
return array(PelTag::IMAGE_WIDTH,
|
|
PelTag::IMAGE_LENGTH,
|
|
PelTag::BITS_PER_SAMPLE,
|
|
PelTag::COMPRESSION,
|
|
PelTag::PHOTOMETRIC_INTERPRETATION,
|
|
PelTag::IMAGE_DESCRIPTION,
|
|
PelTag::MAKE,
|
|
PelTag::MODEL,
|
|
PelTag::STRIP_OFFSETS,
|
|
PelTag::ORIENTATION,
|
|
PelTag::SAMPLES_PER_PIXEL,
|
|
PelTag::ROWS_PER_STRIP,
|
|
PelTag::STRIP_BYTE_COUNTS,
|
|
PelTag::X_RESOLUTION,
|
|
PelTag::Y_RESOLUTION,
|
|
PelTag::PLANAR_CONFIGURATION,
|
|
PelTag::RESOLUTION_UNIT,
|
|
PelTag::TRANSFER_FUNCTION,
|
|
PelTag::SOFTWARE,
|
|
PelTag::DATE_TIME,
|
|
PelTag::ARTIST,
|
|
PelTag::WHITE_POINT,
|
|
PelTag::PRIMARY_CHROMATICITIES,
|
|
PelTag::JPEG_INTERCHANGE_FORMAT,
|
|
PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
PelTag::YCBCR_COEFFICIENTS,
|
|
PelTag::YCBCR_SUB_SAMPLING,
|
|
PelTag::YCBCR_POSITIONING,
|
|
PelTag::REFERENCE_BLACK_WHITE,
|
|
PelTag::COPYRIGHT,
|
|
PelTag::EXIF_IFD_POINTER,
|
|
PelTag::GPS_INFO_IFD_POINTER,
|
|
PelTag::PRINT_IM);
|
|
|
|
case PelIfd::EXIF:
|
|
return array(PelTag::EXPOSURE_TIME,
|
|
PelTag::FNUMBER,
|
|
PelTag::EXPOSURE_PROGRAM,
|
|
PelTag::SPECTRAL_SENSITIVITY,
|
|
PelTag::ISO_SPEED_RATINGS,
|
|
PelTag::OECF,
|
|
PelTag::EXIF_VERSION,
|
|
PelTag::DATE_TIME_ORIGINAL,
|
|
PelTag::DATE_TIME_DIGITIZED,
|
|
PelTag::COMPONENTS_CONFIGURATION,
|
|
PelTag::COMPRESSED_BITS_PER_PIXEL,
|
|
PelTag::SHUTTER_SPEED_VALUE,
|
|
PelTag::APERTURE_VALUE,
|
|
PelTag::BRIGHTNESS_VALUE,
|
|
PelTag::EXPOSURE_BIAS_VALUE,
|
|
PelTag::MAX_APERTURE_VALUE,
|
|
PelTag::SUBJECT_DISTANCE,
|
|
PelTag::METERING_MODE,
|
|
PelTag::LIGHT_SOURCE,
|
|
PelTag::FLASH,
|
|
PelTag::FOCAL_LENGTH,
|
|
PelTag::MAKER_NOTE,
|
|
PelTag::USER_COMMENT,
|
|
PelTag::SUB_SEC_TIME,
|
|
PelTag::SUB_SEC_TIME_ORIGINAL,
|
|
PelTag::SUB_SEC_TIME_DIGITIZED,
|
|
PelTag::XP_TITLE,
|
|
PelTag::XP_COMMENT,
|
|
PelTag::XP_AUTHOR,
|
|
PelTag::XP_KEYWORDS,
|
|
PelTag::XP_SUBJECT,
|
|
PelTag::FLASH_PIX_VERSION,
|
|
PelTag::COLOR_SPACE,
|
|
PelTag::PIXEL_X_DIMENSION,
|
|
PelTag::PIXEL_Y_DIMENSION,
|
|
PelTag::RELATED_SOUND_FILE,
|
|
PelTag::FLASH_ENERGY,
|
|
PelTag::SPATIAL_FREQUENCY_RESPONSE,
|
|
PelTag::FOCAL_PLANE_X_RESOLUTION,
|
|
PelTag::FOCAL_PLANE_Y_RESOLUTION,
|
|
PelTag::FOCAL_PLANE_RESOLUTION_UNIT,
|
|
PelTag::SUBJECT_LOCATION,
|
|
PelTag::EXPOSURE_INDEX,
|
|
PelTag::SENSING_METHOD,
|
|
PelTag::FILE_SOURCE,
|
|
PelTag::SCENE_TYPE,
|
|
PelTag::CFA_PATTERN,
|
|
PelTag::CUSTOM_RENDERED,
|
|
PelTag::EXPOSURE_MODE,
|
|
PelTag::WHITE_BALANCE,
|
|
PelTag::DIGITAL_ZOOM_RATIO,
|
|
PelTag::FOCAL_LENGTH_IN_35MM_FILM,
|
|
PelTag::SCENE_CAPTURE_TYPE,
|
|
PelTag::GAIN_CONTROL,
|
|
PelTag::CONTRAST,
|
|
PelTag::SATURATION,
|
|
PelTag::SHARPNESS,
|
|
PelTag::DEVICE_SETTING_DESCRIPTION,
|
|
PelTag::SUBJECT_DISTANCE_RANGE,
|
|
PelTag::IMAGE_UNIQUE_ID,
|
|
PelTag::INTEROPERABILITY_IFD_POINTER,
|
|
PelTag::GAMMA);
|
|
|
|
case PelIfd::GPS:
|
|
return array(PelTag::GPS_VERSION_ID,
|
|
PelTag::GPS_LATITUDE_REF,
|
|
PelTag::GPS_LATITUDE,
|
|
PelTag::GPS_LONGITUDE_REF,
|
|
PelTag::GPS_LONGITUDE,
|
|
PelTag::GPS_ALTITUDE_REF,
|
|
PelTag::GPS_ALTITUDE,
|
|
PelTag::GPS_TIME_STAMP,
|
|
PelTag::GPS_SATELLITES,
|
|
PelTag::GPS_STATUS,
|
|
PelTag::GPS_MEASURE_MODE,
|
|
PelTag::GPS_DOP,
|
|
PelTag::GPS_SPEED_REF,
|
|
PelTag::GPS_SPEED,
|
|
PelTag::GPS_TRACK_REF,
|
|
PelTag::GPS_TRACK,
|
|
PelTag::GPS_IMG_DIRECTION_REF,
|
|
PelTag::GPS_IMG_DIRECTION,
|
|
PelTag::GPS_MAP_DATUM,
|
|
PelTag::GPS_DEST_LATITUDE_REF,
|
|
PelTag::GPS_DEST_LATITUDE,
|
|
PelTag::GPS_DEST_LONGITUDE_REF,
|
|
PelTag::GPS_DEST_LONGITUDE,
|
|
PelTag::GPS_DEST_BEARING_REF,
|
|
PelTag::GPS_DEST_BEARING,
|
|
PelTag::GPS_DEST_DISTANCE_REF,
|
|
PelTag::GPS_DEST_DISTANCE,
|
|
PelTag::GPS_PROCESSING_METHOD,
|
|
PelTag::GPS_AREA_INFORMATION,
|
|
PelTag::GPS_DATE_STAMP,
|
|
PelTag::GPS_DIFFERENTIAL);
|
|
|
|
case PelIfd::INTEROPERABILITY:
|
|
return array(PelTag::INTEROPERABILITY_INDEX,
|
|
PelTag::INTEROPERABILITY_VERSION,
|
|
PelTag::RELATED_IMAGE_FILE_FORMAT,
|
|
PelTag::RELATED_IMAGE_WIDTH,
|
|
PelTag::RELATED_IMAGE_LENGTH);
|
|
|
|
/* TODO: Where do these tags belong?
|
|
PelTag::FILL_ORDER,
|
|
PelTag::DOCUMENT_NAME,
|
|
PelTag::TRANSFER_RANGE,
|
|
PelTag::JPEG_PROC,
|
|
PelTag::BATTERY_LEVEL,
|
|
PelTag::IPTC_NAA,
|
|
PelTag::INTER_COLOR_PROFILE,
|
|
PelTag::CFA_REPEAT_PATTERN_DIM,
|
|
*/
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the name of an IFD type.
|
|
*
|
|
* @param int one of {@link PelIfd::IFD0}, {@link PelIfd::IFD1},
|
|
* {@link PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
|
|
* PelIfd::INTEROPERABILITY}.
|
|
*
|
|
* @return string the name of type.
|
|
*/
|
|
static function getTypeName($type) {
|
|
switch ($type) {
|
|
case self::IFD0:
|
|
return '0';
|
|
case self::IFD1:
|
|
return '1';
|
|
case self::EXIF:
|
|
return 'Exif';
|
|
case self::GPS:
|
|
return 'GPS';
|
|
case self::INTEROPERABILITY:
|
|
return 'Interoperability';
|
|
default:
|
|
throw new PelIfdException('Unknown IFD type: %d', $type);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the name of this directory.
|
|
*
|
|
* @return string the name of this directory.
|
|
*/
|
|
function getName() {
|
|
return $this->getTypeName($this->type);
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds an entry to the directory.
|
|
*
|
|
* @param PelEntry the entry that will be added.
|
|
*
|
|
* @todo The entry will be identified with its tag, so each
|
|
* directory can only contain one entry with each tag. Is this a
|
|
* bug?
|
|
*/
|
|
function addEntry(PelEntry $e) {
|
|
$this->entries[$e->getTag()] = $e;
|
|
}
|
|
|
|
|
|
/**
|
|
* Does a given tag exist in this IFD?
|
|
*
|
|
* This methods is part of the ArrayAccess SPL interface for
|
|
* overriding array access of objects, it allows you to check for
|
|
* existance of an entry in the IFD:
|
|
*
|
|
* <code>
|
|
* if (isset($ifd[PelTag::FNUMBER]))
|
|
* // ... do something with the F-number.
|
|
* </code>
|
|
*
|
|
* @param PelTag the offset to check.
|
|
*
|
|
* @return boolean whether the tag exists.
|
|
*/
|
|
function offsetExists($tag) {
|
|
return isset($this->entries[$tag]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve a given tag from this IFD.
|
|
*
|
|
* This methods is part of the ArrayAccess SPL interface for
|
|
* overriding array access of objects, it allows you to read entries
|
|
* from the IFD the same was as for an array:
|
|
*
|
|
* <code>
|
|
* $entry = $ifd[PelTag::FNUMBER];
|
|
* </code>
|
|
*
|
|
* @param PelTag the tag to return. It is an error to ask for a tag
|
|
* which is not in the IFD, just like asking for a non-existant
|
|
* array entry.
|
|
*
|
|
* @return PelEntry the entry.
|
|
*/
|
|
function offsetGet($tag) {
|
|
return $this->entries[$tag];
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or update a given tag in this IFD.
|
|
*
|
|
* This methods is part of the ArrayAccess SPL interface for
|
|
* overriding array access of objects, it allows you to add new
|
|
* entries or replace esisting entries by doing:
|
|
*
|
|
* <code>
|
|
* $ifd[PelTag::EXPOSURE_BIAS_VALUE] = $entry;
|
|
* </code>
|
|
*
|
|
* Note that the actual array index passed is ignored! Instead the
|
|
* {@link PelTag} from the entry is used.
|
|
*
|
|
* @param PelTag the offset to update.
|
|
*
|
|
* @param PelEntry the new value.
|
|
*/
|
|
function offsetSet($tag, $e) {
|
|
if ($e instanceof PelEntry) {
|
|
$tag = $e->getTag();
|
|
$this->entries[$tag] = $e;
|
|
} else {
|
|
throw new PelInvalidArgumentException('Argument "%s" must be a PelEntry.', $e);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Unset a given tag in this IFD.
|
|
*
|
|
* This methods is part of the ArrayAccess SPL interface for
|
|
* overriding array access of objects, it allows you to delete
|
|
* entries in the IFD by doing:
|
|
*
|
|
* <code>
|
|
* unset($ifd[PelTag::EXPOSURE_BIAS_VALUE])
|
|
* </code>
|
|
*
|
|
* @param PelTag the offset to delete.
|
|
*/
|
|
function offsetUnset($tag) {
|
|
unset($this->entries[$tag]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve an entry.
|
|
*
|
|
* @param PelTag the tag identifying the entry.
|
|
*
|
|
* @return PelEntry the entry associated with the tag, or null if no
|
|
* such entry exists.
|
|
*/
|
|
function getEntry($tag) {
|
|
if (isset($this->entries[$tag]))
|
|
return $this->entries[$tag];
|
|
else
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns all entries contained in this IFD.
|
|
*
|
|
* @return array an array of {@link PelEntry} objects, or rather
|
|
* descendant classes. The array has {@link PelTag}s as keys
|
|
* and the entries as values.
|
|
*
|
|
* @see getEntry
|
|
* @see getIterator
|
|
*/
|
|
function getEntries() {
|
|
return $this->entries;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an iterator for all entries contained in this IFD.
|
|
*
|
|
* Used with foreach as in
|
|
*
|
|
* <code>
|
|
* foreach ($ifd as $tag => $entry) {
|
|
* // $tag is now a PelTag and $entry is a PelEntry object.
|
|
* }
|
|
* </code>
|
|
*
|
|
* @return Iterator an iterator using the {@link PelTag tags} as
|
|
* keys and the entries as values.
|
|
*/
|
|
function getIterator() {
|
|
return new ArrayIterator($this->entries);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns available thumbnail data.
|
|
*
|
|
* @return string the bytes in the thumbnail, if any. If the IFD
|
|
* does not contain any thumbnail data, the empty string is
|
|
* returned.
|
|
*
|
|
* @todo Throw an exception instead when no data is available?
|
|
*
|
|
* @todo Return the $this->thumb_data object instead of the bytes?
|
|
*/
|
|
function getThumbnailData() {
|
|
if ($this->thumb_data != null)
|
|
return $this->thumb_data->getBytes();
|
|
else
|
|
return '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Make this directory point to a new directory.
|
|
*
|
|
* @param PelIfd the IFD that this directory will point to.
|
|
*/
|
|
function setNextIfd(PelIfd $i) {
|
|
$this->next = $i;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the IFD pointed to by this directory.
|
|
*
|
|
* @return PelIfd the next IFD, following this IFD. If this is the
|
|
* last IFD, null is returned.
|
|
*/
|
|
function getNextIfd() {
|
|
return $this->next;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if this is the last IFD.
|
|
*
|
|
* @return boolean true if there are no following IFD, false
|
|
* otherwise.
|
|
*/
|
|
function isLastIfd() {
|
|
return $this->next == null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a sub-IFD.
|
|
*
|
|
* Any previous sub-IFD of the same type will be overwritten.
|
|
*
|
|
* @param PelIfd the sub IFD. The type of must be one of {@link
|
|
* PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
|
|
* PelIfd::INTEROPERABILITY}.
|
|
*/
|
|
function addSubIfd(PelIfd $sub) {
|
|
$this->sub[$sub->type] = $sub;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return a sub IFD.
|
|
*
|
|
* @param int the type of the sub IFD. This must be one of {@link
|
|
* PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link
|
|
* PelIfd::INTEROPERABILITY}.
|
|
*
|
|
* @return PelIfd the IFD associated with the type, or null if that
|
|
* sub IFD does not exist.
|
|
*/
|
|
function getSubIfd($type) {
|
|
if (isset($this->sub[$type]))
|
|
return $this->sub[$type];
|
|
else
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get all sub IFDs.
|
|
*
|
|
* @return array an associative array with (IFD-type, {@link
|
|
* PelIfd}) pairs.
|
|
*/
|
|
function getSubIfds() {
|
|
return $this->sub;
|
|
}
|
|
|
|
|
|
/**
|
|
* Turn this directory into bytes.
|
|
*
|
|
* This directory will be turned into a byte string, with the
|
|
* specified byte order. The offsets will be calculated from the
|
|
* offset given.
|
|
*
|
|
* @param int the offset of the first byte of this directory.
|
|
*
|
|
* @param PelByteOrder the byte order that should be used when
|
|
* turning integers into bytes. This should be one of {@link
|
|
* PelConvert::LITTLE_ENDIAN} and {@link PelConvert::BIG_ENDIAN}.
|
|
*/
|
|
function getBytes($offset, $order) {
|
|
$bytes = '';
|
|
$extra_bytes = '';
|
|
|
|
Pel::debug('Bytes from IDF will start at offset %d within Exif data',
|
|
$offset);
|
|
|
|
$n = count($this->entries) + count($this->sub);
|
|
if ($this->thumb_data != null) {
|
|
/* We need two extra entries for the thumbnail offset and
|
|
* length. */
|
|
$n += 2;
|
|
}
|
|
|
|
$bytes .= PelConvert::shortToBytes($n, $order);
|
|
|
|
/* Initialize offset of extra data. This included the bytes
|
|
* preceding this IFD, the bytes needed for the count of entries,
|
|
* the entries themselves (and sub entries), the extra data in the
|
|
* entries, and the IFD link.
|
|
*/
|
|
$end = $offset + 2 + 12 * $n + 4;
|
|
|
|
foreach ($this->entries as $tag => $entry) {
|
|
/* Each entry is 12 bytes long. */
|
|
$bytes .= PelConvert::shortToBytes($entry->getTag(), $order);
|
|
$bytes .= PelConvert::shortToBytes($entry->getFormat(), $order);
|
|
$bytes .= PelConvert::longToBytes($entry->getComponents(), $order);
|
|
|
|
/*
|
|
* Size? If bigger than 4 bytes, the actual data is not in
|
|
* the entry but somewhere else.
|
|
*/
|
|
$data = $entry->getBytes($order);
|
|
$s = strlen($data);
|
|
if ($s > 4) {
|
|
Pel::debug('Data size %d too big, storing at offset %d instead.',
|
|
$s, $end);
|
|
$bytes .= PelConvert::longToBytes($end, $order);
|
|
$extra_bytes .= $data;
|
|
$end += $s;
|
|
} else {
|
|
Pel::debug('Data size %d fits.', $s);
|
|
/* Copy data directly, pad with NULL bytes as necessary to
|
|
* fill out the four bytes available.*/
|
|
$bytes .= $data . str_repeat(chr(0), 4 - $s);
|
|
}
|
|
}
|
|
|
|
if ($this->thumb_data != null) {
|
|
Pel::debug('Appending %d bytes of thumbnail data at %d',
|
|
$this->thumb_data->getSize(), $end);
|
|
// TODO: make PelEntry a class that can be constructed with
|
|
// arguments corresponding to the newt four lines.
|
|
$bytes .= PelConvert::shortToBytes(PelTag::JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
$order);
|
|
$bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
|
|
$bytes .= PelConvert::longToBytes(1, $order);
|
|
$bytes .= PelConvert::longToBytes($this->thumb_data->getSize(),
|
|
$order);
|
|
|
|
$bytes .= PelConvert::shortToBytes(PelTag::JPEG_INTERCHANGE_FORMAT,
|
|
$order);
|
|
$bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
|
|
$bytes .= PelConvert::longToBytes(1, $order);
|
|
$bytes .= PelConvert::longToBytes($end, $order);
|
|
|
|
$extra_bytes .= $this->thumb_data->getBytes();
|
|
$end += $this->thumb_data->getSize();
|
|
}
|
|
|
|
|
|
/* Find bytes from sub IFDs. */
|
|
$sub_bytes = '';
|
|
foreach ($this->sub as $type => $sub) {
|
|
if ($type == PelIfd::EXIF)
|
|
$tag = PelTag::EXIF_IFD_POINTER;
|
|
elseif ($type == PelIfd::GPS)
|
|
$tag = PelTag::GPS_INFO_IFD_POINTER;
|
|
elseif ($type == PelIfd::INTEROPERABILITY)
|
|
$tag = PelTag::INTEROPERABILITY_IFD_POINTER;
|
|
|
|
/* Make an aditional entry with the pointer. */
|
|
$bytes .= PelConvert::shortToBytes($tag, $order);
|
|
/* Next the format, which is always unsigned long. */
|
|
$bytes .= PelConvert::shortToBytes(PelFormat::LONG, $order);
|
|
/* There is only one component. */
|
|
$bytes .= PelConvert::longToBytes(1, $order);
|
|
|
|
$data = $sub->getBytes($end, $order);
|
|
$s = strlen($data);
|
|
$sub_bytes .= $data;
|
|
|
|
$bytes .= PelConvert::longToBytes($end, $order);
|
|
$end += $s;
|
|
}
|
|
|
|
/* Make link to next IFD, if any*/
|
|
if ($this->isLastIFD()) {
|
|
$link = 0;
|
|
} else {
|
|
$link = $end;
|
|
}
|
|
|
|
Pel::debug('Link to next IFD: %d', $link);
|
|
|
|
$bytes .= PelConvert::longtoBytes($link, $order);
|
|
|
|
$bytes .= $extra_bytes . $sub_bytes;
|
|
|
|
if (!$this->isLastIfd())
|
|
$bytes .= $this->next->getBytes($end, $order);
|
|
|
|
return $bytes;
|
|
}
|
|
|
|
|
|
/**
|
|
* Turn this directory into text.
|
|
*
|
|
* @return string information about the directory, mainly for
|
|
* debugging.
|
|
*/
|
|
function __toString() {
|
|
$str = Pel::fmt("Dumping IFD %s with %d entries...\n",
|
|
$this->getName(), count($this->entries));
|
|
|
|
foreach ($this->entries as $entry)
|
|
$str .= $entry->__toString();
|
|
|
|
$str .= Pel::fmt("Dumping %d sub IFDs...\n", count($this->sub));
|
|
|
|
foreach ($this->sub as $type => $ifd)
|
|
$str .= $ifd->__toString();
|
|
|
|
if ($this->next != null)
|
|
$str .= $this->next->__toString();
|
|
|
|
return $str;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
?>
|