????
Your IP : 3.144.119.207
<?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, 2008 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
*/
namespace lsolesen\pel;
/**
* Classes for dealing with Exif IFDs.
*
* @author Martin Geisler <mgeisler@users.sourceforge.net>
* @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 <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;
/**
* Canon Maker Notes IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_MAKER_NOTES = 5;
/**
* Canon Camera Settings IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_CAMERA_SETTINGS = 6;
/**
* Canon Shot Info IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_SHOT_INFO = 7;
/**
* Canon Shot Info IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_PANORAMA = 8;
/**
* Canon Shot Info IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_PICTURE_INFO = 9;
/**
* Canon Shot Info IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_FILE_INFO = 10;
/**
* Canon Shot Info IFD.
*
* Pass this to the constructor when creating an IFD which will be
* the canon maker notes sub-IFD.
*/
const CANON_CUSTOM_FUNCTIONS = 11;
private const TYPE_NAMES = [
self::IFD0 => '0',
self::IFD1 => '1',
self::EXIF => 'Exif',
self::GPS => 'GPS',
self::INTEROPERABILITY => 'Interoperability',
self::CANON_MAKER_NOTES => 'Canon Maker Notes',
self::CANON_CAMERA_SETTINGS => 'Canon Camera Settings',
self::CANON_SHOT_INFO => 'Canon Shot Information',
self::CANON_PANORAMA => 'Canon Panorama Information',
self::CANON_PICTURE_INFO => 'Canon Picture Information',
self::CANON_FILE_INFO => 'Canon File Information',
self::CANON_CUSTOM_FUNCTIONS => 'Canon Custom Functions'
];
private const VALID_TAGS = [
self::IFD0 => [
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::PREDICTOR,
PelTag::WHITE_POINT,
PelTag::PRIMARY_CHROMATICITIES,
PelTag::EXTRA_SAMPLES,
PelTag::SAMPLE_FORMAT,
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,
PelTag::RATING_PERCENT,
PelTag::APPLICATION_NOTES
],
self::EXIF => [
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::OFFSET_TIME,
PelTag::OFFSET_TIME_ORIGINAL,
PelTag::OFFSET_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
],
self::GPS => [
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
],
self::INTEROPERABILITY => [
PelTag::INTEROPERABILITY_INDEX,
PelTag::INTEROPERABILITY_VERSION,
PelTag::RELATED_IMAGE_FILE_FORMAT,
PelTag::RELATED_IMAGE_WIDTH,
PelTag::RELATED_IMAGE_LENGTH
],
self::CANON_MAKER_NOTES => [
PelTag::CANON_CAMERA_SETTINGS,
PelTag::CANON_FOCAL_LENGTH,
PelTag::CANON_SHOT_INFO,
PelTag::CANON_PANORAMA,
PelTag::CANON_IMAGE_TYPE,
PelTag::CANON_FIRMWARE_VERSION,
PelTag::CANON_FILE_NUMBER,
PelTag::CANON_OWNER_NAME,
PelTag::CANON_SERIAL_NUMBER,
PelTag::CANON_CAMERA_INFO,
PelTag::CANON_CUSTOM_FUNCTIONS,
PelTag::CANON_MODEL_ID,
PelTag::CANON_PICTURE_INFO,
PelTag::CANON_THUMBNAIL_IMAGE_VALID_AREA,
PelTag::CANON_SERIAL_NUMBER_FORMAT,
PelTag::CANON_SUPER_MACRO,
PelTag::CANON_FIRMWARE_REVISION,
PelTag::CANON_AF_INFO,
PelTag::CANON_ORIGINAL_DECISION_DATA_OFFSET,
PelTag::CANON_WHITE_BALANCE_TABLE,
PelTag::CANON_LENS_MODEL,
PelTag::CANON_INTERNAL_SERIAL_NUMBER,
PelTag::CANON_DUST_REMOVAL_DATA,
PelTag::CANON_CUSTOM_FUNCTIONS_2,
PelTag::CANON_PROCESSING_INFO,
PelTag::CANON_MEASURED_COLOR,
PelTag::CANON_COLOR_SPACE,
PelTag::CANON_VRD_OFFSET,
PelTag::CANON_SENSOR_INFO,
PelTag::CANON_COLOR_DATA
],
self::CANON_CAMERA_SETTINGS => [
PelTag::CANON_CS_MACRO,
PelTag::CANON_CS_SELF_TIMER,
PelTag::CANON_CS_QUALITY,
PelTag::CANON_CS_FLASH_MODE,
PelTag::CANON_CS_DRIVE_MODE,
PelTag::CANON_CS_FOCUS_MODE,
PelTag::CANON_CS_RECORD_MODE,
PelTag::CANON_CS_IMAGE_SIZE,
PelTag::CANON_CS_EASY_MODE,
PelTag::CANON_CS_DIGITAL_ZOOM,
PelTag::CANON_CS_CONTRAST,
PelTag::CANON_CS_SATURATION,
PelTag::CANON_CS_SHARPNESS,
PelTag::CANON_CS_ISO_SPEED,
PelTag::CANON_CS_METERING_MODE,
PelTag::CANON_CS_FOCUS_TYPE,
PelTag::CANON_CS_AF_POINT,
PelTag::CANON_CS_EXPOSURE_PROGRAM,
PelTag::CANON_CS_LENS_TYPE,
PelTag::CANON_CS_LENS,
PelTag::CANON_CS_SHORT_FOCAL,
PelTag::CANON_CS_FOCAL_UNITS,
PelTag::CANON_CS_MAX_APERTURE,
PelTag::CANON_CS_MIN_APERTURE,
PelTag::CANON_CS_FLASH_ACTIVITY,
PelTag::CANON_CS_FLASH_DETAILS,
PelTag::CANON_CS_FOCUS_CONTINUOUS,
PelTag::CANON_CS_AE_SETTING,
PelTag::CANON_CS_IMAGE_STABILIZATION,
PelTag::CANON_CS_DISPLAY_APERTURE,
PelTag::CANON_CS_ZOOM_SOURCE_WIDTH,
PelTag::CANON_CS_ZOOM_TARGET_WIDTH,
PelTag::CANON_CS_SPOT_METERING_MODE,
PelTag::CANON_CS_PHOTO_EFFECT,
PelTag::CANON_CS_MANUAL_FLASH_OUTPUT,
PelTag::CANON_CS_COLOR_TONE,
PelTag::CANON_CS_SRAW_QUALITY
],
self::CANON_SHOT_INFO => [
PelTag::CANON_SI_ISO_SPEED,
PelTag::CANON_SI_MEASURED_EV,
PelTag::CANON_SI_TARGET_APERTURE,
PelTag::CANON_SI_TARGET_SHUTTER_SPEED,
PelTag::CANON_SI_WHITE_BALANCE,
PelTag::CANON_SI_SLOW_SHUTTER,
PelTag::CANON_SI_SEQUENCE,
PelTag::CANON_SI_AF_POINT_USED,
PelTag::CANON_SI_FLASH_BIAS,
PelTag::CANON_SI_AUTO_EXPOSURE_BRACKETING,
PelTag::CANON_SI_SUBJECT_DISTANCE,
PelTag::CANON_SI_APERTURE_VALUE,
PelTag::CANON_SI_SHUTTER_SPEED_VALUE,
PelTag::CANON_SI_MEASURED_EV2,
PelTag::CANON_SI_CAMERA_TYPE,
PelTag::CANON_SI_AUTO_ROTATE,
PelTag::CANON_SI_ND_FILTER
],
self::CANON_PANORAMA => [
PelTag::CANON_PA_PANORAMA_FRAME,
PelTag::CANON_PA_PANORAMA_DIRECTION
],
self::CANON_PICTURE_INFO => [
PelTag::CANON_PI_IMAGE_WIDTH,
PelTag::CANON_PI_IMAGE_HEIGHT,
PelTag::CANON_PI_IMAGE_WIDTH_AS_SHOT,
PelTag::CANON_PI_IMAGE_HEIGHT_AS_SHOT,
PelTag::CANON_PI_AF_POINTS_USED,
PelTag::CANON_PI_AF_POINTS_USED_20D
],
self::CANON_FILE_INFO => [
PelTag::CANON_FI_FILE_NUMBER,
PelTag::CANON_FI_BRACKET_MODE,
PelTag::CANON_FI_BRACKET_VALUE,
PelTag::CANON_FI_BRACKET_SHOT_NUMBER,
PelTag::CANON_FI_RAW_JPG_QUALITY,
PelTag::CANON_FI_RAW_JPG_SIZE,
PelTag::CANON_FI_NOISE_REDUCTION,
PelTag::CANON_FI_WB_BRACKET_MODE,
PelTag::CANON_FI_WB_BRACKET_VALUE_AB,
PelTag::CANON_FI_WB_BRACKET_VALUE_GM,
PelTag::CANON_FI_FILTER_EFFECT,
PelTag::CANON_FI_TONING_EFFECT,
PelTag::CANON_FI_MACRO_MAGNIFICATION,
PelTag::CANON_FI_LIVE_VIEW_SHOOTING,
PelTag::CANON_FI_FOCUS_DISTANCE_UPPER,
PelTag::CANON_FI_FOCUS_DISTANCE_LOWER,
PelTag::CANON_FI_FLASH_EXPOSURE_LOCK
]
/*
* 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,
*/
];
/**
* The maker notes held by this directory.
*
* Stores information of the MakerNotes IFD.
* Available and required keys are: parent, data, components and offset
*
* @var array
*/
private $maker_notes = [];
/**
* 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 = [];
/**
* 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 = [];
/**
* 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 integer $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.
* @throws PelIfdException
*/
public function __construct($type)
{
if (! array_key_exists($type, self::TYPE_NAMES)) {
throw new PelIfdException('Unknown IFD type: %d', $type);
}
$this->type = $type;
}
/**
* Stores Maker Notes data for an IFD (Probably PelIfd::EXIF only).
*
* @param PelIfd $parent
* the parent PelIfd of the current PelIfd
* @param PelDataWindow $data
* the data window that will provide the data.
* @param PelIfd $parent
* the components in the entry.
* @param integer $offset
* the offset within the window where the directory will
* be found.
*/
public function setMakerNotes($parent, $data, $components, $offset)
{
$this->maker_notes = [
'parent' => $parent,
'data' => $data,
'components' => $components,
'offset' => $offset
];
}
/**
* Returns the Maker Notes data for an IFD (Probably PelIfd::EXIF only).
*
* @return array The maker_notes of IDF
*/
public function getMakerNotes()
{
return $this->maker_notes;
}
/**
* Load data into a Image File Directory (IFD).
*
* @param PelDataWindow $d
* the data window that will provide the data.
* @param integer $offset
* the offset within the window where the directory will
* be found.
* @throws PelException
* @throws PelEntryUndefined
* @throws PelUnexpectedFormatException
* @throws PelWrongComponentCountException
*/
public function load(PelDataWindow $d, $offset)
{
$starting_offset = $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:
case PelTag::MAKER_NOTE:
$type = null;
$components = $d->getLong($offset + 12 * $i + 4);
$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;
} elseif ($tag == PelTag::MAKER_NOTE) {
// Store maker notes infos, because we need PelTag::MAKE of PelIfd::IFD0 for MakerNotes
// Thus MakerNotes will be loaded at the end of loading PelIfd::IFD0
$this->setMakerNotes($this, $d, $components, $o);
$this->loadSingleValue($d, $offset, $i, $tag);
break;
}
if ($type === null) {
Pel::maybeThrow(new PelIfdException('Type not detected for Tag: %d.', $tag));
} elseif ($starting_offset == $o) {
Pel::maybeThrow(new PelIfdException('Bogus offset to next IFD: %d, same as offset being loaded from.', $o));
} else {
$ifd = new PelIfd($type);
try {
$ifd->load($d, $o);
$this->sub[$type] = $ifd;
} catch (PelDataWindowOffsetException $e) {
Pel::maybeThrow(new PelIfdException($e->getMessage()));
}
}
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:
$this->loadSingleValue($d, $offset, $i, $tag);
break;
}
}
/* Offset to next IFD */
$o = $d->getLong((int) ($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.');
}
// Check if we finished loading IFD0 and EXIF IFD is set (EXIF IFD holds the MakerNotes)
if ($this->type == PelIfd::IFD0 && isset($this->sub[PelIfd::EXIF])) {
// Get MakerNotes from EXIF IFD and check if they are set
$mk = $this->sub[PelIfd::EXIF]->getMakerNotes();
if (! empty($mk) && count($mk) > 0) {
// get Make tag and load maker notes if tag is valid
$manufacturer = $this->getEntry(PelTag::MAKE);
if ($manufacturer !== null) {
$manufacturer = $manufacturer->getValue();
$mkNotes = PelMakerNotes::createMakerNotesFromManufacturer($manufacturer, $mk['parent'], $mk['data'], $mk['components'], $mk['offset']);
if ($mkNotes !== null) {
// remove pre-loaded undefined MakerNotes
$mk['parent']->offsetUnset(PelTag::MAKER_NOTE);
$mkNotes->load();
}
}
}
}
}
/**
* Load a single value which didn't match any special {@link PelTag}.
*
* This method will add a single value given by the {@link PelDataWindow} and it's offset ($offset) and element counter ($i).
*
* 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.
*
* @param PelDataWindow $d
* the data window that will provide the data.
* @param integer $offset
* the offset within the window where the directory will
* be found.
* @param integer $i
* the element's position in the {@link PelDataWindow} $d.
* @param integer $tag
* the tag of the entry as defined in {@link PelTag}.
* @throws PelException
* @throws PelEntryUndefined
* @throws PelUnexpectedFormatException
* @throws PelWrongComponentCountException
*/
public function loadSingleValue($d, $offset, $i, $tag)
{
$format = $d->getShort($offset + 12 * $i + 2);
$components = $d->getLong($offset + 12 * $i + 4);
$size = PelFormat::getSize($format);
if (is_string($size)) {
Pel::maybeThrow(new PelException('Invalid format %s', $format));
return;
}
try {
/*
* 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 = $size * $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();
}
$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();
// }
}
/**
* Load a single value which didn't match any special {@link PelTag}.
*
* This method will add a single value given by the {@link PelDataWindow} and it's offset ($offset) and element counter ($i).
*
* 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.
*
* @param integer $type
* the type of the ifd
* @param PelDataWindow $data
* the data window that will provide the data.
* @param integer $offset
* the offset within the window where the directory will
* be found.
* @param integer $size
* the size in bytes of the maker notes section
* @param integer $i
* the element's position in the {@link PelDataWindow} $data.
* @param integer $format
* the format {@link PelFormat} of the entry.
* @throws PelException
* @throws PelDataWindowWindowException
* @throws PelInvalidArgumentException
*/
public function loadSingleMakerNotesValue($type, PelDataWindow $data, $offset, $size, $i, $format)
{
$elemSize = PelFormat::getSize($format);
if ($size > 0) {
$subdata = $data->getClone($offset + $i * $elemSize, $elemSize);
} else {
$subdata = new PelDataWindow();
}
try {
$entry = $this->newEntryFromData($i + 1, $format, 1, $subdata);
$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();
// }
}
/**
* 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 integer $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.
* @throws PelException
* @throws PelEntryUndefined
* @throws PelUnexpectedFormatException
* @throws PelWrongComponentCountException
*/
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, (float) $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);
}
return new PelEntryWindowsString($tag, $data->getBytes(), true);
}
// 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:
// cut off string after the first nul byte
$canonicalString = strstr($data->getBytes(0), "\0", true);
if ($canonicalString !== false) {
return new PelEntryAscii($tag, $canonicalString);
}
// TODO throw exception if string isn't nul-terminated
return new PelEntryAscii($tag, $data->getBytes(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 integer $offset
* the offset into the data.
* @param integer $length
* the length of the thumbnail.
* @throws PelIfdException
* @throws PelDataWindowWindowException
*/
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. */
try {
$this->setThumbnail($d->getClone($offset, $length));
} catch (PelDataWindowWindowException $e) {
Pel::maybeThrow(new PelIfdException($e->getMessage()));
}
}
}
/**
* 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.
* @throws PelIfdException
*/
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 integer $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()
{
$tp = $this->type;
if ($tp === self::IFD1) {
// return the same for IFD0 and IFD1
$tp = self::IFD0;
}
if (array_key_exists($tp, self::VALID_TAGS)) {
return self::VALID_TAGS[$tp];
}
}
/**
* Get the name of an IFD type.
*
* @param integer $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)
{
if (array_key_exists($type, self::TYPE_NAMES)) {
return self::TYPE_NAMES[$type];
}
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:
*
* <code>
* if (isset($ifd[PelTag::FNUMBER]))
* // ... do something with the F-number.
* </code>
*
* @param integer $tag
* the offset to check.
* @return boolean whether the tag exists.
*/
public function offsetExists($tag): bool
{
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 integer $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): PelEntry
{
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 integer $tag
* unused.
* @param PelEntry $e
* the new value.
* @throws PelInvalidArgumentException
*/
public function offsetSet($tag, $e): void
{
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 integer $tag
* the offset to delete.
*/
public function offsetUnset($tag): void
{
unset($this->entries[$tag]);
}
/**
* Retrieve an entry.
*
* @param integer $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 PelIfd::getEntry
* @see PelIfd::getIterator
*/
public 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 \ArrayIterator an iterator using the {@link PelTag tags} as
* keys and the entries as values.
*/
public function getIterator(): \ArrayIterator
{
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.
* @throws PelDataWindowOffsetException
* @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 integer $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 integer $offset
* the offset of the first byte of this directory.
* @param boolean $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 $ifd) {
$str .= $ifd->__toString();
}
if ($this->next !== null) {
$str .= $this->next->__toString();
}
return $str;
}
}