X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=vendor%2Flsolesen%2Fpel%2Fsrc%2FPelIfd.php;fp=vendor%2Flsolesen%2Fpel%2Fsrc%2FPelIfd.php;h=86b517642e0ba1dab8ff6bea4f42acb824ac5f3e;hp=0000000000000000000000000000000000000000;hb=0bf8d09d2542548982e81a441b1f16e75873a04f;hpb=74df008bdbb3a11eeea356744f39b802369bda3c diff --git a/vendor/lsolesen/pel/src/PelIfd.php b/vendor/lsolesen/pel/src/PelIfd.php new file mode 100644 index 000000000..86b517642 --- /dev/null +++ b/vendor/lsolesen/pel/src/PelIfd.php @@ -0,0 +1,1221 @@ + + * @license http://www.gnu.org/licenses/gpl.html GNU General Public + * License (GPL) + * @package PEL + */ + +/** + * 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 + * @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. + */ + public 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 $d + * the data window that will provide the data. + * + * @param int $offset + * the offset within the window where the directory will + * be found. + */ + public 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); + $this->addEntry($entry); + } 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 integer $tag + * the tag of the entry as defined in {@link PelTag}. + * + * @param integer $format + * the format of the entry as defined in {@link PelFormat}. + * + * @param int $components + * the components in the entry. + * + * @param PelDataWindow $data + * the data which will be used to construct the + * entry. + * + * @return PelEntry a newly created entry, holding the data given. + */ + public 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(), ' ')); + if (! isset($v[1])) { + Pel::maybeThrow(new PelException('Invalid copyright: %s', $data->getBytes())); + // when not in strict mode, set empty copyright and continue + $v[1] = ''; + } + 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))); + } + // this point can not be reached + 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); + } + // This point can be reached! Continue with default. + 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, rtrim($data->getBytes(0), "\0")); + + 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 $d + * the data from which the thumbnail will be + * extracted. + * + * @param int $offset + * the offset into the data. + * + * @param int $length + * 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 $d + * the thumbnail data. + */ + public 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}. + */ + public 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 $tag + * the tag. + * + * @return boolean true if the tag is considered valid in this IFD, + * false otherwise. + * + * @see getValidTags() + */ + public 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. + */ + public 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::DOCUMENT_NAME, + 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, + PelTag::XP_TITLE, + PelTag::XP_COMMENT, + PelTag::XP_AUTHOR, + PelTag::XP_KEYWORDS, + PelTag::XP_SUBJECT, + PelTag::RATING + ); + + 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::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::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 $type + * one of {@link PelIfd::IFD0}, {@link PelIfd::IFD1}, + * {@link PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link + * PelIfd::INTEROPERABILITY}. + * + * @return string the name of type. + */ + public 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. + */ + public function getName() + { + return $this->getTypeName($this->type); + } + + /** + * Adds an entry to the directory. + * + * @param PelEntry $e + * the entry that will be added. If the entry is not + * valid in this IFD (as per {@link isValidTag()}) an + * {@link PelInvalidDataException} is thrown. + * + * @todo The entry will be identified with its tag, so each + * directory can only contain one entry with each tag. Is this a + * bug? + */ + public function addEntry(PelEntry $e) + { + if ($this->isValidTag($e->getTag())) { + $e->setIfdType($this->type); + $this->entries[$e->getTag()] = $e; + } else { + throw new PelInvalidDataException("IFD %s cannot hold\n%s", $this->getName(), $e->__toString()); + } + } + + /** + * 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: + * + * + * if (isset($ifd[PelTag::FNUMBER])) + * // ... do something with the F-number. + * + * + * @param PelTag $tag + * the offset to check. + * + * @return boolean whether the tag exists. + */ + public 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: + * + * + * $entry = $ifd[PelTag::FNUMBER]; + * + * + * @param PelTag $tag + * 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. + */ + public 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: + * + * + * $ifd[PelTag::EXPOSURE_BIAS_VALUE] = $entry; + * + * + * Note that the actual array index passed is ignored! Instead the + * {@link PelTag} from the entry is used. + * + * @param PelTag $tag + * the offset to update. + * + * @param PelEntry $e + * the new value. + */ + public 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: + * + * + * unset($ifd[PelTag::EXPOSURE_BIAS_VALUE]) + * + * + * @param PelTag $tag + * the offset to delete. + */ + public function offsetUnset($tag) + { + unset($this->entries[$tag]); + } + + /** + * Retrieve an entry. + * + * @param PelTag $tag + * the tag identifying the entry. + * + * @return PelEntry the entry associated with the tag, or null if no + * such entry exists. + */ + public 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 + */ + public function getEntries() + { + return $this->entries; + } + + /** + * Return an iterator for all entries contained in this IFD. + * + * Used with foreach as in + * + * + * foreach ($ifd as $tag => $entry) { + * // $tag is now a PelTag and $entry is a PelEntry object. + * } + * + * + * @return Iterator an iterator using the {@link PelTag tags} as + * keys and the entries as values. + */ + public 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? + */ + public function getThumbnailData() + { + if ($this->thumb_data !== null) { + return $this->thumb_data->getBytes(); + } else { + return ''; + } + } + + /** + * Make this directory point to a new directory. + * + * @param PelIfd $i + * the IFD that this directory will point to. + */ + public 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. + */ + public function getNextIfd() + { + return $this->next; + } + + /** + * Check if this is the last IFD. + * + * @return boolean true if there are no following IFD, false + * otherwise. + */ + public function isLastIfd() + { + return $this->next === null; + } + + /** + * Add a sub-IFD. + * + * Any previous sub-IFD of the same type will be overwritten. + * + * @param PelIfd $sub + * the sub IFD. The type of must be one of {@link + * PelIfd::EXIF}, {@link PelIfd::GPS}, or {@link + * PelIfd::INTEROPERABILITY}. + */ + public function addSubIfd(PelIfd $sub) + { + $this->sub[$sub->type] = $sub; + } + + /** + * Return a sub IFD. + * + * @param int $type + * 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. + */ + public 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. + */ + public 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 $offset + * the offset of the first byte of this directory. + * + * @param PelByteOrder $order + * 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}. + */ + public 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; + } else { + // PelConvert::BIG_ENDIAN is the default used by PelConvert + $tag = PelConvert::BIG_ENDIAN; + } + /* 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. + */ + public 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; + } +}