/****************************************************************************** * * Filename: EXIF.php * * Description: Provides functions for reading and writing EXIF Information * to/from an APP1 segment of a JPEG file * Unfortunately, because EXIF data may be distributed anywhere * throughout an image file, rather than just being in one block, * it is impossible to pass just a string containing only the EXIF * information. Hence it is neccessary to be able to seek to * any point in the file. This causes the HTTP and FTP wrappers * not to work - i.e. the EXIF functions will only work with local * files. * To work on an internet file, copy it locally to start with: * * $newfilename = tempnam ( $dir, "tmpexif" ); * copy ( "http://whatever.com", $newfilename ); * * * Author: Evan Hunter * * Date: 30/7/2004 * * Project: PHP JPEG Metadata Toolkit * * Revision: 1.00 * * URL: http://electronics.ozhiker.com * * Copyright: Copyright Evan Hunter 2004 * * License: This file is part of the PHP JPEG Metadata Toolkit. * * The PHP JPEG Metadata Toolkit 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. * * The PHP JPEG Metadata Toolkit 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 the PHP JPEG Metadata Toolkit; if not, * write to the Free Software Foundation, Inc., 59 Temple * Place, Suite 330, Boston, MA 02111-1307 USA * * If you require a different license for commercial or other * purposes, please contact the author: evan@ozhiker.com * ******************************************************************************/ // TODO : Thoroughly test the functions for writing EXIF segments // TODO : Test EXIF on TIFF Files // TODO : Figure out a way to allow EXIF to function normally with HTTP and FTP wrappers // TODO : Implement EXIF decoding of Device Setting Description field // TODO : Implement EXIF decoding of SpatialFrequencyResponse field // TODO : Implement EXIF decoding of OECF field // TODO : Implement EXIF decoding of SubjectArea field /****************************************************************************** * * Initialisation * ******************************************************************************/ if ( !isset( $GLOBALS['HIDE_UNKNOWN_TAGS'] ) ) $GLOBALS['HIDE_UNKNOWN_TAGS']= FALSE; if ( !isset( $GLOBALS['SHOW_BINARY_DATA_HEX'] ) ) $GLOBALS['SHOW_BINARY_DATA_HEX'] = FALSE; if ( !isset( $GLOBALS['SHOW_BINARY_DATA_TEXT'] ) ) $GLOBALS['SHOW_BINARY_DATA_TEXT'] = FALSE; include_once 'EXIF_Tags.php'; include_once 'EXIF_Makernote.php'; include_once 'PIM.php'; include_once 'Unicode.php'; include_once 'JPEG.php'; include_once 'IPTC.php'; /****************************************************************************** * * Function: get_EXIF_JPEG * * Description: Retrieves information from a Exchangeable Image File Format (EXIF) * APP1 segment and returns it in an array. * * Parameters: filename - the filename of the JPEG image to process * * Returns: OutputArray - Array of EXIF records * FALSE - If an error occured in decoding * ******************************************************************************/ function get_EXIF_JPEG( $filename ) { // get the JPEG headers $jpeg_header_data = get_jpeg_header_data( $filename ); // Flag that an EXIF segment has not been found yet $EXIF_Location = -1; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // If we find an APP1 header, if ( strcmp ( $jpeg_header_data[$i]['SegName'], "APP1" ) == 0 ) { // And if it has the EXIF label, if ( ( strncmp ( $jpeg_header_data[$i]['SegData'], "Exif\x00\x00", 6) == 0 ) || ( strncmp ( $jpeg_header_data[$i]['SegData'], "Exif\x00\xFF", 6) == 0 ) ) // For some reason, some files have a faulty EXIF name which has a 0xFF in it { // Save the location of the EXIF segment $EXIF_Location = $i; } } } // Check if an EXIF segment was found if ( $EXIF_Location == -1 ) { // Couldn't find any EXIF block to decode return FALSE; } $filehnd = @fopen($filename, 'rb'); // Check if the file opened successfully if ( ! $filehnd ) { // Could't open the file - exit echo "
Could not open file $filename
\n"; return FALSE; } fseek( $filehnd, $jpeg_header_data[$EXIF_Location]['SegDataStart'] + 6 ); // Decode the Exif segment into an array and return it $exif_data = process_TIFF_Header( $filehnd, "TIFF" ); // Close File fclose($filehnd); return $exif_data; } /****************************************************************************** * End of Function: get_EXIF_JPEG ******************************************************************************/ /****************************************************************************** * * Function: put_EXIF_JPEG * * Description: Stores information into a Exchangeable Image File Format (EXIF) * APP1 segment from an EXIF array. * * WARNING: Because the EXIF standard allows pointers to data * outside the APP1 segment, if there are any such pointers in * a makernote, this function will DAMAGE them since it will not * be aware that there is an external pointer. This will often * happen with Makernotes that include an embedded thumbnail. * This damage could be prevented where makernotes can be decoded, * but currently this is not implemented. * * * Parameters: exif_data - The array of EXIF data to insert into the JPEG header * jpeg_header_data - The JPEG header into which the EXIF data * should be stored, as from get_jpeg_header_data * * Returns: jpeg_header_data - JPEG header array with the EXIF segment inserted * FALSE - If an error occured * ******************************************************************************/ function put_EXIF_JPEG( $exif_data, $jpeg_header_data ) { // pack the EXIF data into its proper format for a JPEG file $packed_data = get_TIFF_Packed_Data( $exif_data ); if ( $packed_data === FALSE ) { return $jpeg_header_data; } $packed_data = "Exif\x00\x00$packed_data"; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // If we find an APP1 header, if ( strcmp ( $jpeg_header_data[$i]['SegName'], "APP1" ) == 0 ) { // And if it has the EXIF label, if ( ( strncmp ( $jpeg_header_data[$i]['SegData'], "Exif\x00\x00", 6) == 0 ) || ( strncmp ( $jpeg_header_data[$i]['SegData'], "Exif\x00\xFF", 6) == 0 ) ) // For some reason, some files have a faulty EXIF name which has a 0xFF in it { // Found a preexisting EXIF block - Replace it with the new one and return. $jpeg_header_data[$i]['SegData'] = $packed_data; return $jpeg_header_data; } } } // No preexisting segment segment found, insert a new one at the start of the header data. // Determine highest position of an APP segment at or below APP3, so we can put the // new APP3 at this position $highest_APP = -1; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // Check if we have found an APP segment at or below APP3, if ( ( $jpeg_header_data[$i]['SegType'] >= 0xE0 ) && ( $jpeg_header_data[$i]['SegType'] <= 0xE3 ) ) { // Found an APP segment at or below APP12 $highest_APP = $i; } } // No preexisting EXIF block found, insert a new one at the start of the header data. array_splice($jpeg_header_data, $highest_APP + 1 , 0, array( array( "SegType" => 0xE1, "SegName" => "APP1", "SegDesc" => $GLOBALS[ "JPEG_Segment_Descriptions" ][ 0xE1 ], "SegData" => $packed_data ) ) ); return $jpeg_header_data; } /****************************************************************************** * End of Function: put_EXIF_JPEG ******************************************************************************/ /****************************************************************************** * * Function: get_Meta_JPEG * * Description: Retrieves information from a Meta APP3 segment and returns it * in an array. Uses information supplied by the * get_jpeg_header_data function. * The Meta segment has the same format as an EXIF segment, but * uses different tags * * Parameters: filename - the filename of the JPEG image to process * * Returns: OutputArray - Array of Meta records * FALSE - If an error occured in decoding * ******************************************************************************/ function get_Meta_JPEG( $filename ) { // get the JPEG headers $jpeg_header_data = get_jpeg_header_data( $filename ); // Flag that an Meta segment has not been found yet $Meta_Location = -1; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // If we find an APP3 header, if ( strcmp ( $jpeg_header_data[$i]['SegName'], "APP3" ) == 0 ) { // And if it has the Meta label, if ( ( strncmp ( $jpeg_header_data[$i]['SegData'], "Meta\x00\x00", 6) == 0 ) || ( strncmp ( $jpeg_header_data[$i]['SegData'], "META\x00\x00", 6) == 0 ) ) { // Save the location of the Meta segment $Meta_Location = $i; } } } // Check if an EXIF segment was found if ( $Meta_Location == -1 ) { // Couldn't find any Meta block to decode return FALSE; } $filehnd = @fopen($filename, 'rb'); // Check if the file opened successfully if ( ! $filehnd ) { // Could't open the file - exit echo "Could not open file $filename
\n"; return FALSE; } fseek( $filehnd, $jpeg_header_data[$Meta_Location]['SegDataStart'] + 6 ); // Decode the Meta segment into an array and return it $meta = process_TIFF_Header( $filehnd, "Meta" ); // Close File fclose($filehnd); return $meta; } /****************************************************************************** * End of Function: get_Meta ******************************************************************************/ /****************************************************************************** * * Function: put_Meta_JPEG * * Description: Stores information into a Meta APP3 segment from a Meta array. * * * WARNING: Because the Meta (EXIF) standard allows pointers to data * outside the APP1 segment, if there are any such pointers in * a makernote, this function will DAMAGE them since it will not * be aware that there is an external pointer. This will often * happen with Makernotes that include an embedded thumbnail. * This damage could be prevented where makernotes can be decoded, * but currently this is not implemented. * * * Parameters: meta_data - The array of Meta data to insert into the JPEG header * jpeg_header_data - The JPEG header into which the Meta data * should be stored, as from get_jpeg_header_data * * Returns: jpeg_header_data - JPEG header array with the Meta segment inserted * FALSE - If an error occured * ******************************************************************************/ function put_Meta_JPEG( $meta_data, $jpeg_header_data ) { // pack the Meta data into its proper format for a JPEG file $packed_data = get_TIFF_Packed_Data( $meta_data ); if ( $packed_data === FALSE ) { return $jpeg_header_data; } $packed_data = "Meta\x00\x00$packed_data"; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // If we find an APP1 header, if ( strcmp ( $jpeg_header_data[$i]['SegName'], "APP3" ) == 0 ) { // And if it has the Meta label, if ( ( strncmp ( $jpeg_header_data[$i]['SegData'], "Meta\x00\x00", 6) == 0 ) || ( strncmp ( $jpeg_header_data[$i]['SegData'], "META\x00\x00", 6) == 0 ) ) { // Found a preexisting Meta block - Replace it with the new one and return. $jpeg_header_data[$i]['SegData'] = $packed_data; return $jpeg_header_data; } } } // No preexisting segment segment found, insert a new one at the start of the header data. // Determine highest position of an APP segment at or below APP3, so we can put the // new APP3 at this position $highest_APP = -1; //Cycle through the header segments for( $i = 0; $i < count( $jpeg_header_data ); $i++ ) { // Check if we have found an APP segment at or below APP3, if ( ( $jpeg_header_data[$i]['SegType'] >= 0xE0 ) && ( $jpeg_header_data[$i]['SegType'] <= 0xE3 ) ) { // Found an APP segment at or below APP12 $highest_APP = $i; } } // No preexisting Meta block found, insert a new one at the start of the header data. array_splice($jpeg_header_data, $highest_APP + 1 , 0, array( array( "SegType" => 0xE3, "SegName" => "APP3", "SegDesc" => $GLOBALS[ "JPEG_Segment_Descriptions" ][ 0xE1 ], "SegData" => $packed_data ) ) ); return $jpeg_header_data; } /****************************************************************************** * End of Function: put_Meta_JPEG ******************************************************************************/ /****************************************************************************** * * Function: Interpret_EXIF_to_HTML * * Description: Generates html detailing the contents an APP1 EXIF array * which was retrieved with a get_EXIF_.... function. * Can also be used for APP3 Meta arrays. * * Parameters: Exif_array - the EXIF array,as read from get_EXIF_.... * filename - the name of the Image file being processed ( used * by scripts which displays EXIF thumbnails) * * Returns: output_str - A string containing the HTML * ******************************************************************************/ function Interpret_EXIF_to_HTML( $Exif_array, $filename ) { // Create the string to receive the html output $output_str = ""; // Check if the array to process is valid if ( $Exif_array === FALSE ) { // Exif Array is not valid - abort processing return $output_str; } // Ouput the heading according to what type of tags were used in processing if ( $Exif_array[ 'Tags Name' ] == "TIFF" ) { $output_str .= "Error: Corrupted EXIF
\n"; return FALSE; } $Output_Array[] = $IFD_Array; } while ( $Next_Offset != 0 ); // Until the Next IFD Offset is zero // return resulting array return $Output_Array ; } /****************************************************************************** * End of Function: read_Multiple_IFDs ******************************************************************************/ /****************************************************************************** * * Internal Function: read_IFD_universal * * Description: Reads and interprets a standard or Non-standard Image File * Directory (IFD), and returns the entries in an array * * Parameters: filehnd - a handle for the image file being read, positioned at the start * of the IFD * Tiff_offset - The offset of the TIFF header from the start of the file * Byte_Align - either "MM" or "II" indicating Motorola or Intel Byte alignment * Tag_Definitions_Name - The name of the Tag Definitions group within the global array IFD_Tag_Definitions * local_offsets - True indicates that offset data should be interpreted as being relative to the start of the currrent entry * False (normal) indicates offests are relative to start of Tiff header as per IFD standard * read_next_ptr - True (normal) indicates that a pointer to the next IFD should be read at the end of the IFD * False indicates that no pointer follows the IFD * * Returns: OutputArray - Array of IFD entries * Next_Offset - Offset to next IFD (zero = no next IFD) * ******************************************************************************/ function read_IFD_universal( $filehnd, $Tiff_offset, $Byte_Align, $Tag_Definitions_Name, $local_offsets = FALSE, $read_next_ptr = TRUE ) { if ( ( $filehnd == NULL ) || ( feof( $filehnd ) ) ) { return array (FALSE , 0); } // Record the Name of the Tag Group used for this IFD in the output array $OutputArray[ 'Tags Name' ] = $Tag_Definitions_Name; // Record the offset of the TIFF header in the output array $OutputArray[ 'Tiff Offset' ] = $Tiff_offset; // First 2 bytes of IFD are number of entries in the IFD $No_Entries_str = network_safe_fread( $filehnd, 2 ); $No_Entries = get_IFD_Data_Type( $No_Entries_str, 3, $Byte_Align ); // If the data is corrupt, the number of entries may be huge, which will cause errors // This is often caused by a lack of a Next-IFD pointer if ( $No_Entries> 10000 ) { // Huge number of entries - abort echo "Error: huge number of EXIF entries - EXIF is probably Corrupted
\n"; return array ( FALSE , 0); } // If the data is corrupt or just stupid, the number of entries may zero, // Indicate this by returning false if ( $No_Entries === 0 ) { // No entries - abort return array ( FALSE , 0); } // Save the file position where first IFD record starts as non-standard offsets // need to know this to calculate an absolute offset $IFD_first_rec_pos = ftell( $filehnd ); // Read in the IFD structure $IFD_Data = network_safe_fread( $filehnd, 12 * $No_Entries ); // Check if the entire IFD was able to be read if ( strlen( $IFD_Data ) != (12 * $No_Entries) ) { // Couldn't read the IFD Data properly, Some Casio files have no Next IFD pointer, hence cause this error echo "Error: EXIF Corrupted
\n"; return array(FALSE, 0); } // Last 4 bytes of a standard IFD are the offset to the next IFD // Some NON-Standard IFD implementations do not have this, hence causing problems if it is read // If the Next IFD pointer has been requested to be read, if ( $read_next_ptr ) { // Read the pointer to the next IFD $Next_Offset_str = network_safe_fread( $filehnd, 4 ); $Next_Offset = get_IFD_Data_Type( $Next_Offset_str, 4, $Byte_Align ); } else { // Otherwise set the pointer to zero ( no next IFD ) $Next_Offset = 0; } // Initialise current position to the start $pos = 0; // Loop for reading IFD entries for ( $i = 0; $i < $No_Entries; $i++ ) { // First 2 bytes of IFD entry are the tag number ( Unsigned Short ) $Tag_No_str = substr( $IFD_Data, $pos, 2 ); $Tag_No = get_IFD_Data_Type( $Tag_No_str, 3, $Byte_Align ); $pos += 2; // Next 2 bytes of IFD entry are the data format ( Unsigned Short ) $Data_Type_str = substr( $IFD_Data, $pos, 2 ); $Data_Type = get_IFD_Data_Type( $Data_Type_str, 3, $Byte_Align ); $pos += 2; // If Datatype is not between 1 and 12, then skip this entry, it is probably corrupted or custom if (( $Data_Type > 12 ) || ( $Data_Type < 1 ) ) { $pos += 8; continue 1; // Stop trying to process the tag any further and skip to the next one } // Next 4 bytes of IFD entry are the data count ( Unsigned Long ) $Data_Count_str = substr( $IFD_Data, $pos, 4 ); $Data_Count = get_IFD_Data_Type( $Data_Count_str, 4, $Byte_Align ); $pos += 4; if ( $Data_Count > 100000 ) { echo "Error: huge EXIF data count - EXIF is probably Corrupted
\n"; // Some Casio files have no Next IFD pointer, hence cause errors return array ( FALSE , 0); } // Total Data size is the Data Count multiplied by the size of the Data Type $Total_Data_Size = $GLOBALS['IFD_Data_Sizes'][ $Data_Type ] * $Data_Count; $Data_Start_pos = -1; // If the total data size is larger than 4 bytes, then the data part is the offset to the real data if ( $Total_Data_Size > 4 ) { // Not enough room for data - offset provided instead $Data_Offset_str = substr( $IFD_Data, $pos, 4 ); $Data_Start_pos = get_IFD_Data_Type( $Data_Offset_str, 4, $Byte_Align ); // In some NON-STANDARD makernotes, the offset is relative to the start of the current IFD entry if ( $local_offsets ) { // This is a NON-Standard IFD, seek relative to the start of the current tag fseek( $filehnd, $IFD_first_rec_pos + $pos - 8 + $Data_Start_pos ); } else { // This is a normal IFD, seek relative to the start of the TIFF header fseek( $filehnd, $Tiff_offset + $Data_Start_pos ); } // Read the data block from the offset position $DataStr = network_safe_fread( $filehnd, $Total_Data_Size ); } else { // The data block is less than 4 bytes, and is provided in the IFD entry, so read it $DataStr = substr( $IFD_Data, $pos, $Total_Data_Size ); } // Increment the position past the data $pos += 4; // Now create the entry for output array $Data_Array = array( ); // Read the data items from the data block if ( ( $Data_Type != 2 ) && ( $Data_Type != 7 ) ) { // The data type is Numerical, Read the data items from the data block for ( $j = 0; $j < $Data_Count; $j++ ) { $Part_Data_Str = substr( $DataStr, $j * $GLOBALS['IFD_Data_Sizes'][ $Data_Type ], $GLOBALS['IFD_Data_Sizes'][ $Data_Type ] ); $Data_Array[] = get_IFD_Data_Type( $Part_Data_Str, $Data_Type, $Byte_Align ); } } elseif ( $Data_Type == 2 ) { // The data type is String(s) (type 2) // Strip the last terminating Null $DataStr = substr( $DataStr, 0, strlen($DataStr)-1 ); // Split the data block into multiple strings whereever there is a Null $Data_Array = explode( "\x00", $DataStr ); } else { // The data type is Unknown (type 7) // Do nothing to data $Data_Array = $DataStr; } // If this is a Sub-IFD entry, if ( ( array_key_exists( $Tag_No, $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name] ) ) && ( "SubIFD" == $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Type'] ) ) { // This is a Sub-IFD entry, go and process the data forming Sub-IFD and use its output array as the new data for this entry fseek( $filehnd, $Tiff_offset + $Data_Array[0] ); $Data_Array = read_Multiple_IFDs( $filehnd, $Tiff_offset, $Byte_Align, $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Tags Name'] ); } $desc = ""; $units = ""; // Check if this tag exists in the list of tag definitions, if ( array_key_exists ( $Tag_No, $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name]) ) { if ( array_key_exists ( 'Description', $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ] ) ) { $desc = $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Description']; } if ( array_key_exists ( 'Units', $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ] ) ) { $units = $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Units']; } // Tag exists in definitions, append details to output array $OutputArray[ $Tag_No ] = array ( "Tag Number" => $Tag_No, "Tag Name" => $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Name'], "Tag Description" => $desc, "Data Type" => $Data_Type, "Type" => $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag_No ]['Type'], "Units" => $units, "Data" => $Data_Array ); } else { // Tag doesnt exist in definitions, append unknown details to output array $OutputArray[ $Tag_No ] = array ( "Tag Number" => $Tag_No, "Tag Name" => "Unknown Tag #" . $Tag_No, "Tag Description" => "", "Data Type" => $Data_Type, "Type" => "Unknown", "Units" => "", "Data" => $Data_Array ); } // Some information of type "Unknown" (type 7) might require information about // how it's position and byte alignment in order to be decoded if ( $Data_Type == 7 ) { $OutputArray[ $Tag_No ]['Offset'] = $Data_Start_pos; $OutputArray[ $Tag_No ]['Byte Align'] = $Byte_Align; } //////////////////////////////////////////////////////////////////////// // Special Data handling //////////////////////////////////////////////////////////////////////// // Check if this is a Print Image Matching entry if ( $OutputArray[ $Tag_No ]['Type'] == "PIM" ) { // This is a Print Image Matching entry, decode it. $OutputArray[ $Tag_No ] = Decode_PIM( $OutputArray[ $Tag_No ], $Tag_Definitions_Name ); } // Interpret the entry into a text string using a custom interpreter $text_val = get_Tag_Text_Value( $OutputArray[ $Tag_No ], $Tag_Definitions_Name ); // Check if a text string was generated if ( $text_val !== FALSE ) { // A string was generated, append it to the output array entry $OutputArray[ $Tag_No ]['Text Value'] = $text_val; $OutputArray[ $Tag_No ]['Decoded'] = TRUE; } else { // A string was NOT generated, append a generic string to the output array entry $OutputArray[ $Tag_No ]['Text Value'] = get_IFD_value_as_text( $OutputArray[ $Tag_No ] ) . " " . $units; $OutputArray[ $Tag_No ]['Decoded'] = FALSE; } // Check if this entry is the Maker Note if ( ( $Tag_Definitions_Name == "EXIF" ) && ( $Tag_No == 37500 ) ) { // Save some extra information which will allow Makernote Decoding with the output array entry $OutputArray[ $Tag_No ]['Offset'] = $Data_Start_pos; $OutputArray[ $Tag_No ][ 'Tiff Offset' ] = $Tiff_offset; $OutputArray[ $Tag_No ]['ByteAlign'] = $Byte_Align; // Save a pointer to this entry for Maker note processing later $GLOBALS[ "Maker_Note_Tag" ] = & $OutputArray[ $Tag_No ]; } // Check if this is a IPTC/NAA Record within the EXIF IFD if ( ( ( $Tag_Definitions_Name == "EXIF" ) || ( $Tag_Definitions_Name == "TIFF" ) ) && ( $Tag_No == 33723 ) ) { // This is a IPTC/NAA Record, interpret it and put result in the data for this entry $OutputArray[ $Tag_No ]['Data'] = get_IPTC( $DataStr ); $OutputArray[ $Tag_No ]['Decoded'] = TRUE; } // Exif Thumbnail // Check that both the thumbnail length and offset entries have been processed, // and that this is one of them if ( ( ( ( $Tag_No == 513 ) && ( array_key_exists( 514, $OutputArray ) ) ) || ( ( $Tag_No == 514 ) && ( array_key_exists( 513, $OutputArray ) ) ) ) && ( $Tag_Definitions_Name == "TIFF" ) ) { // Seek to the start of the thumbnail using the offset entry fseek( $filehnd, $Tiff_offset + $OutputArray[513]['Data'][0] ); // Read the thumbnail data, and replace the offset data with the thumbnail $OutputArray[513]['Data'] = network_safe_fread( $filehnd, $OutputArray[514]['Data'][0] ); } // Casio Thumbnail // Check that both the thumbnail length and offset entries have been processed, // and that this is one of them if ( ( ( ( $Tag_No == 0x0004 ) && ( array_key_exists( 0x0003, $OutputArray ) ) ) || ( ( $Tag_No == 0x0003 ) && ( array_key_exists( 0x0004, $OutputArray ) ) ) ) && ( $Tag_Definitions_Name == "Casio Type 2" ) ) { // Seek to the start of the thumbnail using the offset entry fseek( $filehnd, $Tiff_offset + $OutputArray[0x0004]['Data'][0] ); // Read the thumbnail data, and replace the offset data with the thumbnail $OutputArray[0x0004]['Data'] = network_safe_fread( $filehnd, $OutputArray[0x0003]['Data'][0] ); } // Minolta Thumbnail // Check that both the thumbnail length and offset entries have been processed, // and that this is one of them if ( ( ( ( $Tag_No == 0x0088 ) && ( array_key_exists( 0x0089, $OutputArray ) ) ) || ( ( $Tag_No == 0x0089 ) && ( array_key_exists( 0x0088, $OutputArray ) ) ) ) && ( $Tag_Definitions_Name == "Olympus" ) ) { // Seek to the start of the thumbnail using the offset entry fseek( $filehnd, $Tiff_offset + $OutputArray[0x0088]['Data'][0] ); // Read the thumbnail data, and replace the offset data with the thumbnail $OutputArray[0x0088]['Data'] = network_safe_fread( $filehnd, $OutputArray[0x0089]['Data'][0] ); // Sometimes the minolta thumbnail data is empty (or the offset is corrupt, which results in the same thing) // Check if the thumbnail data exists if ( $OutputArray[0x0088]['Data'] != "" ) { // Thumbnail exists // Minolta Thumbnails are missing their first 0xFF for some reason, // which is replaced with some weird character, so fix this $OutputArray[0x0088]['Data']{0} = "\xFF"; } else { // Thumbnail doesnt exist - make it obvious $OutputArray[0x0088]['Data'] = FALSE; } } } // Return the array of IFD entries and the offset to the next IFD return array ($OutputArray , $Next_Offset); } /****************************************************************************** * End of Function: read_IFD_universal ******************************************************************************/ /****************************************************************************** * * Internal Function: get_Tag_Text_Value * * Description: Attempts to interpret an IFD entry into a text string using the * information in the IFD_Tag_Definitions global array. * * Parameters: Tag - The IFD entry to process * Tag_Definitions_Name - The name of the tag definitions to use from within the IFD_Tag_Definitions global array * * Returns: String - if the tag was successfully decoded into a text string * FALSE - if the tag could not be decoded using the information * in the IFD_Tag_Definitions global array * ******************************************************************************/ function get_Tag_Text_Value( $Tag, $Tag_Definitions_Name ) { // Check what format the entry is specified as if ( $Tag['Type'] == "String" ) { // Format is Text String // If "Unknown" (type 7) data type, if ( $Tag['Data Type'] == 7 ) { // Return data as is. return $Tag['Data']; } else { // Otherwise return the default string value of the datatype return get_IFD_value_as_text( $Tag ); } } else if ( $Tag['Type'] == "Character Coded String" ) { // Format is Character Coded String (First 8 characters indicate coding scheme) // Convert Data to a string if ( $Tag['Data Type'] == 7 ) { // If it is type "Unknown" (type 7) use data as is $data = $Tag['Data']; } else { // Otherwise use the default string value of the datatype $data = get_IFD_value_as_text( $Tag ); } // Some implementations allow completely data with no Coding Scheme Name, // so we need to handle this to avoid errors if ( trim( $data ) == "" ) { return ""; } // Extract the Coding Scheme Name from the first 8 characters $char_code = substr( $data, 0, 8 ); // Extract the Data part from after the first 8 characters $characters = substr( $data, 8 ); // Check coding scheme and interpret as neccessary if ( $char_code === "ASCII\x00\x00\x00" ) { // ASCII coding - return data as is. return $characters; } elseif ( ( $char_code === "UNICODE\x00" ) || ( $char_code === "Unicode\x00" ) ) // Note lowercase is non standard { // Unicode coding - interpret and return result. return xml_UTF16_clean( $characters, TRUE ); } else { // Unknown coding - return string indicating this return "Unsupported character coding : \"$char_code\"\n\"" . trim($characters) . "\""; } break; } else if ( $Tag['Type'] == "Numeric" ) { // Format is numeric - return default text value with any required units text appended if ( array_key_exists ( 'Units', $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag["Tag Number"] ] ) ) { $units = $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag["Tag Number"] ]['Units']; } else { $units = ""; } return get_IFD_value_as_text( $Tag ) . " " . $units; } else if ( $Tag['Type'] == "Lookup" ) { // Format is a Lookup Table // Get a numeric value to use in lookup if ( is_array( $Tag['Data'] ) ) { // If data is an array, use first element $first_val = $Tag['Data'][0]; } else if ( is_string( $Tag['Data'] ) ) { // If data is a string, use the first character $first_val = ord($Tag['Data']{0}); } else { // Otherwise use the data as is $first_val = $Tag['Data']; } // Check if the data value exists in the lookup table for this IFD entry if ( array_key_exists( $first_val, $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag["Tag Number"] ] ) ) { // Data value exists in lookup table - return the matching string return $GLOBALS[ "IFD_Tag_Definitions" ][$Tag_Definitions_Name][ $Tag["Tag Number"] ][ $first_val ]; } else { // Data value doesnt exist in lookup table - return explanation string return "Unknown Reserved value $first_val "; } } else if ( $Tag['Type'] == "Special" ) { // Format is special - interpret to text with special handlers return get_Special_Tag_Text_Value( $Tag, $Tag_Definitions_Name ); } else if ( $Tag['Type'] == "PIM" ) { // Format is Print Image Matching info - interpret with custom handler return get_PIM_Text_Value( $Tag, $Tag_Definitions_Name ); } else if ( $Tag['Type'] == "SubIFD" ) { // Format is a Sub-IFD - this has no text value return ""; } else { // Unknown Format - Couldn't interpret using the IFD_Tag_Definitions global array information return FALSE; } } /****************************************************************************** * End of Function: get_Tag_Text_Value ******************************************************************************/ /****************************************************************************** * * Internal Function: get_Special_Tag_Text_Value * * Description: Interprets an IFD entry marked as "Special" in the IFD_Tag_Definitions * global array into a text string using custom handlers * * Parameters: Tag - The IFD entry to process * Tag_Definitions_Name - The name of the tag definitions to use from within the IFD_Tag_Definitions global array * * Returns: String - if the tag was successfully decoded into a text string * FALSE - if the tag could not be decoded * ******************************************************************************/ function get_Special_Tag_Text_Value( $Tag, $Tag_Definitions_Name ) { // Check what type of IFD is being decoded if ( $Tag_Definitions_Name == "TIFF" ) { // This is a TIFF IFD (bottom level) // Check what tag number the IFD entry has. switch ( $Tag['Tag Number'] ) { case 530: // YCbCr Sub Sampling Entry // Data contains two numerical values if ( ( $Tag['Data'][0] == 2 ) && ( $Tag['Data'][1] == 1 ) ) { // Values are 2,1 - hence YCbCr 4:2:2 return "YCbCr 4:2:2 ratio of chrominance components to the luminance components"; } elseif ( ( $Tag['Data'][0] == 2 ) && ( $Tag['Data'][1] == 2 ) ) { // Values are 2,2 - hence YCbCr 4:2:0 return "YCbCr 4:2:0 ratio of chrominance components to the luminance components"; } else { // Other values are unknown return "Unknown Reserved value (" . $Tag['Data'][0] . ")"; } break; default: return FALSE; } } else if ( $Tag_Definitions_Name == "EXIF" ) { // This is an EXIF IFD // Check what tag number the IFD entry has. switch ( $Tag['Tag Number'] ) { case 37121: // Components configuration // Data contains 4 numerical values indicating component type $output_str = ""; // Cycle through each component for ( $Num = 0; $Num < 4; $Num++ ) { // Construct first part of text string $output_str .= "Component " . ( $Num + 1 ) . ": "; // Construct second part of text string via // lookup using numerical value $value = ord( $Tag['Data']{$Num} ); switch( $value ) { case 0: $output_str .= "Does not exist\n"; break; case 1: $output_str .= "Y (Luminance)\n"; break; case 2: $output_str .= "Cb (Chroma minus Blue)\n"; break; case 3: $output_str .= "Cr (Chroma minus Red)\n"; break; case 4: $output_str .= "Red\n"; break; case 5: $output_str .= "Green\n"; break; case 6: $output_str .= "Blue\n"; break; default: $output_str .= "Unknown value $value\n"; }; } // Return the completed string return $output_str; break; case 41730: // Colour Filter Array Pattern // The first two characters are a SHORT for Horizontal repeat pixel unit - $n_max = get_IFD_Data_Type( substr( $Tag['Data'], 0, 2 ), 3, $Tag['Byte Align'] ); // The next two characters are a SHORT for Vertical repeat pixel unit - $m_max = get_IFD_Data_Type( substr( $Tag['Data'], 2, 2 ), 3, $Tag['Byte Align'] ); // At least one camera type appears to have byte reversed values for N_Max and M_Max // Check if they need reversing if ( $n_max > 256 ) { $n_max = $n_max/256 + 256*($n_max%256); } if ( $m_max > 256 ) { $m_max = $m_max/256 + 256*($m_max%256); } $output_str = ""; // Cycle through all the elements in the resulting 2 dimensional array, for( $m = 1; $m <= $m_max; $m++ ) { for( $n = 1; $n <= $n_max; $n++ ) { // Append text from a lookup table according to // the value read for this element switch ( ord($Tag['Data']{($n_max*($m-1)+$n+3)}) ) { case 0: $output_str .= "RED "; break; case 1: $output_str .= "GREEN "; break; case 2: $output_str .= "BLUE "; break; case 3: $output_str .= "CYAN "; break; case 4: $output_str .= "MAGENTA "; break; case 5: $output_str .= "YELLOW "; break; case 6: $output_str .= "WHITE "; break; default: $output_str .= "Unknown "; break; }; }; $output_str .= "\n"; }; // Return the resulting string return $output_str; break; default: return FALSE; } } else { // Unknown IFD type, see if it is part of a makernote return get_Makernote_Text_Value( $Tag, $Tag_Definitions_Name ); } } /****************************************************************************** * End of Function: get_Tag_Text_Value ******************************************************************************/ /****************************************************************************** * * Function: interpret_IFD * * Description: Generates html detailing the contents a single IFD. * * Parameters: IFD_array - the array containing an IFD * filename - the name of the Image file being processed ( used * by scripts which displays EXIF thumbnails) * * Returns: output_str - A string containing the HTML * ******************************************************************************/ function interpret_IFD( $IFD_array, $filename ) { // Create the output string with the table tag $output_str = "Error - ASCII Strings should not be processed in get_IFD_Data_Type
\n"; return "Error Should never get here"; //explode( "\x00", $input_data ); } // Check if this is a Unsigned rational type elseif ( $data_type == 5 ) { // This is a Unsigned rational type // Check the byte alignment to see if the bytes need to be reversed if ( $Byte_Align == "MM" ) { // Motorola MSB first byte aligment // Unpack the Numerator and denominator and return them return unpack( 'NNumerator/NDenominator', $input_data ); } else { // Intel LSB first byte aligment // Unpack the Numerator and denominator and return them return unpack( 'VNumerator/VDenominator', $input_data ); } } // Check if this is a Signed Byte, Signed Short or Signed Long elseif ( ( $data_type == 6 ) || ( $data_type == 8 ) || ( $data_type == 9 ) ) { // This is a Signed Byte, Signed Short or Signed Long // Check the byte alignment to see if the bytes need to be reversed if ( $Byte_Align == "II" ) { //Intel format, reverse the bytes $input_data = strrev ( $input_data ); } // Convert the binary string to an Unsigned number $value = hexdec( bin2hex( $input_data ) ); // Convert to signed number // Check if it is a Byte above 128 (i.e. a negative number) if ( ( $data_type == 6 ) && ( $value > 128 ) ) { // number should be negative - make it negative return $value - 256; } // Check if it is a Short above 32767 (i.e. a negative number) if ( ( $data_type == 8 ) && ( $value > 32767 ) ) { // number should be negative - make it negative return $value - 65536; } // Check if it is a Long above 2147483648 (i.e. a negative number) if ( ( $data_type == 9 ) && ( $value > 2147483648 ) ) { // number should be negative - make it negative return $value - 4294967296; } // Return the signed number return $value; } // Check if this is Undefined type elseif ( $data_type == 7 ) { // Custom Data - Do nothing return $input_data; } // Check if this is a Signed Rational type elseif ( $data_type == 10 ) { // This is a Signed Rational type // Signed Long not available with endian in unpack , use unsigned and convert // Check the byte alignment to see if the bytes need to be reversed if ( $Byte_Align == "MM" ) { // Motorola MSB first byte aligment // Unpack the Numerator and denominator $value = unpack( 'NNumerator/NDenominator', $input_data ); } else { // Intel LSB first byte aligment // Unpack the Numerator and denominator $value = unpack( 'VNumerator/VDenominator', $input_data ); } // Convert the numerator to a signed number // Check if it is above 2147483648 (i.e. a negative number) if ( $value['Numerator'] > 2147483648 ) { // number is negative $value['Numerator'] -= 4294967296; } // Convert the denominator to a signed number // Check if it is above 2147483648 (i.e. a negative number) if ( $value['Denominator'] > 2147483648 ) { // number is negative $value['Denominator'] -= 4294967296; } // Return the Signed Rational value return $value; } // Check if this is a Float type elseif ( $data_type == 11 ) { // IEEE 754 Float // TODO - EXIF - IFD datatype Float not implemented yet return "FLOAT NOT IMPLEMENTED YET"; } // Check if this is a Double type elseif ( $data_type == 12 ) { // IEEE 754 Double // TODO - EXIF - IFD datatype Double not implemented yet return "DOUBLE NOT IMPLEMENTED YET"; } else { // Error - Invalid Datatype return "Invalid Datatype $data_type"; } } /****************************************************************************** * End of Function: get_IFD_Data_Type ******************************************************************************/ /****************************************************************************** * * Function: put_IFD_Data_Type * * Description: Encodes an IFD field from a value to a binary data string, using * information supplied about the data type and byte alignment of * the stored data. * * Parameters: input_data - an IFD data value, numeric or string * data_type - a number representing the IFD datatype as per the * TIFF 6.0 specification: * 1 = Unsigned 8-bit Byte * 2 = ASCII String * 3 = Unsigned 16-bit Short * 4 = Unsigned 32-bit Long * 5 = Unsigned 2x32-bit Rational * 6 = Signed 8-bit Byte * 7 = Undefined * 8 = Signed 16-bit Short * 9 = Signed 32-bit Long * 10 = Signed 2x32-bit Rational * 11 = 32-bit Float * 12 = 64-bit Double * Byte_Align - Indicates the byte alignment of the data. * MM = Motorola, MSB first, Big Endian * II = Intel, LSB first, Little Endian * * Returns: output - the packed binary string of the data * ******************************************************************************/ function put_IFD_Data_Type( $input_data, $data_type, $Byte_Align ) { // Process according to the datatype switch ( $data_type ) { case 1: // Unsigned Byte - return character as is return chr($input_data); break; case 2: // ASCII String // Return the string with terminating null return $input_data . "\x00"; break; case 3: // Unsigned Short // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the short and return return pack( "v", $input_data ); } else { // Motorola/Big Endian - pack the short and return return pack( "n", $input_data ); } break; case 4: // Unsigned Long // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the long and return return pack( "V", $input_data ); } else { // Motorola/Big Endian - pack the long and return return pack( "N", $input_data ); } break; case 5: // Unsigned Rational // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the two longs and return return pack( "VV", $input_data['Numerator'], $input_data['Denominator'] ); } else { // Motorola/Big Endian - pack the two longs and return return pack( "NN", $input_data['Numerator'], $input_data['Denominator'] ); } break; case 6: // Signed Byte // Check if number is negative if ( $input_data < 0 ) { // Number is negative - return signed character return chr( $input_data + 256 ); } else { // Number is positive - return character return chr( $input_data ); } break; case 7: // Unknown - return as is return $input_data; break; case 8: // Signed Short // Check if number is negative if ( $input_data < 0 ) { // Number is negative - make signed value $input_data = $input_data + 65536; } // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the short and return return pack( "v", $input_data ); } else { // Motorola/Big Endian - pack the short and return return pack( "n", $input_data ); } break; case 9: // Signed Long // Check if number is negative if ( $input_data < 0 ) { // Number is negative - make signed value $input_data = $input_data + 4294967296; } // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the long and return return pack( "v", $input_data ); } else { // Motorola/Big Endian - pack the long and return return pack( "n", $input_data ); } break; case 10: // Signed Rational // Check if numerator is negative if ( $input_data['Numerator'] < 0 ) { // Number is numerator - make signed value $input_data['Numerator'] = $input_data['Numerator'] + 4294967296; } // Check if denominator is negative if ( $input_data['Denominator'] < 0 ) { // Number is denominator - make signed value $input_data['Denominator'] = $input_data['Denominator'] + 4294967296; } // Check byte alignment if ( $Byte_Align == "II" ) { // Intel/Little Endian - pack the two longs and return return pack( "VV", $input_data['Numerator'], $input_data['Denominator'] ); } else { // Motorola/Big Endian - pack the two longs and return return pack( "NN", $input_data['Numerator'], $input_data['Denominator'] ); } break; case 11: // Float // IEEE 754 Float // TODO - EXIF - IFD datatype Float not implemented yet return "FLOAT NOT IMPLEMENTED YET"; break; case 12: // Double // IEEE 754 Double // TODO - EXIF - IFD datatype Double not implemented yet return "DOUBLE NOT IMPLEMENTED YET"; break; default: // Error - Invalid Datatype return "Invalid Datatype $data_type"; break; } // Shouldn't get here return FALSE; } /****************************************************************************** * End of Function: put_IFD_Data_Type ******************************************************************************/ /****************************************************************************** * * Function: get_IFD_value_as_text * * Description: Decodes an IFD field value from a binary data string, using * information supplied about the data type and byte alignment of * the stored data. * This function should be used for all datatypes except ASCII strings * * Parameters: input_data - a binary data string containing the IFD value, * must be exact length of the value * data_type - a number representing the IFD datatype as per the * TIFF 6.0 specification: * 1 = Unsigned 8-bit Byte * 2 = ASCII String * 3 = Unsigned 16-bit Short * 4 = Unsigned 32-bit Long * 5 = Unsigned 2x32-bit Rational * 6 = Signed 8-bit Byte * 7 = Undefined * 8 = Signed 16-bit Short * 9 = Signed 32-bit Long * 10 = Signed 2x32-bit Rational * 11 = 32-bit Float * 12 = 64-bit Double * Byte_Align - Indicates the byte alignment of the data. * MM = Motorola, MSB first, Big Endian * II = Intel, LSB first, Little Endian * * Returns: output - the value of the data (string or numeric) * ******************************************************************************/ function get_IFD_value_as_text( $Exif_Tag ) { // Create a string to receive the output text $output_str = ""; // Select Processing method according to the datatype switch ($Exif_Tag['Data Type']) { case 1 : // Unsigned Byte case 3 : // Unsigned Short case 4 : // Unsigned Long case 6 : // Signed Byte case 8 : // Signed Short case 9 : // Signed Long // Cycle through each of the values for this tag foreach ( $Exif_Tag['Data'] as $val ) { // Check that this isn't the first value, if ( $output_str != "" ) { // This isn't the first value, Add a Comma and Newline to the output $output_str .= ",\n"; } // Add the Value to the output $output_str .= $val; } break; case 2 : // ASCII // Append all the strings together, separated by Newlines $output_str .= implode ( "\n", $Exif_Tag['Data']); break; case 5 : // Unsigned Rational case 10: // Signed Rational // Cycle through each of the values for this tag foreach ( $Exif_Tag['Data'] as $val ) { // Check that this isn't the first value, if ( $output_str != "" ) { // This isn't the first value, Add a Comma and Newline to the output $output_str .= ",\n"; } // Add the Full Value to the output $output_str .= $val['Numerator'] ."/" . $val['Denominator']; // Check if division by zero might be a problem if ( $val['Denominator'] != 0 ) { // Denominator is not zero, Add the Decimal Value to the output text $output_str .= " (" . ($val['Numerator'] / $val['Denominator']) . ")"; } } break; case 11: // Float case 12: // Double // TODO - EXIF - IFD datatype Double and Float not implemented yet $output_str .= "Float and Double not implemented yet"; break; case 7 : // Undefined // Unless the User has asked to see the raw binary data, this // type should not be displayed // Check if the user has requested to see the binary data in hex if ( $GLOBALS['SHOW_BINARY_DATA_HEX'] == TRUE) { // User has requested to see the binary data in hex // Add the value in hex $output_str .= "( " . strlen( $Exif_Tag['Data'] ) . " bytes of binary data ): " . bin2hex( $Exif_Tag['Data'] ) ; } // Check if the user has requested to see the binary data as is else if ( $GLOBALS['SHOW_BINARY_DATA_TEXT'] == TRUE) { // User has requested to see the binary data as is // Add the value as is $output_str .= "( " . strlen( $Exif_Tag['Data'] ) . " bytes of binary data ): " . $Exif_Tag['Data'] ; } else { // User has NOT requested to see binary data, // Add a message indicating the number of bytes to the output $output_str .= "( " . strlen( $Exif_Tag['Data'] ) . " bytes of binary data ) " ; } break; default : // Error - Unknown IFD datatype $output_str .= "Error - Exif tag data type (" . $Exif_Tag['Data Type'] .") is invalid"; break; } // Return the resulting text string return $output_str; } /****************************************************************************** * End of Function: get_IFD_value_as_text ******************************************************************************/ /****************************************************************************** * Global Variable: IFD_Data_Sizes * * Contents: The sizes (in bytes) of each EXIF IFD Datatype, indexed by * their datatype number * ******************************************************************************/ $GLOBALS['IFD_Data_Sizes'] = array( 1 => 1, // Unsigned Byte 2 => 1, // ASCII String 3 => 2, // Unsigned Short 4 => 4, // Unsigned Long 5 => 8, // Unsigned Rational 6 => 1, // Signed Byte 7 => 1, // Undefined 8 => 2, // Signed Short 9 => 4, // Signed Long 10 => 8, // Signed Rational 11 => 4, // Float 12 => 8 ); // Double /****************************************************************************** * End of Global Variable: IFD_Data_Sizes ******************************************************************************/ ?>