MediaWiki  REL1_21
GIFMetadataExtractor.php
Go to the documentation of this file.
00001 <?php
00034 class GIFMetadataExtractor {
00035         static $gif_frame_sep;
00036         static $gif_extension_sep;
00037         static $gif_term;
00038 
00039         const VERSION = 1;
00040 
00041         // Each sub-block is less than or equal to 255 bytes.
00042         // Most of the time its 255 bytes, except for in XMP
00043         // blocks, where it's usually between 32-127 bytes each.
00044         const MAX_SUBBLOCKS = 262144; // 5mb divided by 20.
00045 
00051         static function getMetadata( $filename ) {
00052                 self::$gif_frame_sep = pack( "C", ord( "," ) );
00053                 self::$gif_extension_sep = pack( "C", ord( "!" ) );
00054                 self::$gif_term = pack( "C", ord( ";" ) );
00055 
00056                 $frameCount = 0;
00057                 $duration = 0.0;
00058                 $isLooped = false;
00059                 $xmp = "";
00060                 $comment = array();
00061 
00062                 if ( !$filename ) {
00063                         throw new Exception( "No file name specified" );
00064                 } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
00065                         throw new Exception( "File $filename does not exist" );
00066                 }
00067 
00068                 $fh = fopen( $filename, 'rb' );
00069 
00070                 if ( !$fh ) {
00071                         throw new Exception( "Unable to open file $filename" );
00072                 }
00073 
00074                 // Check for the GIF header
00075                 $buf = fread( $fh, 6 );
00076                 if ( !( $buf == 'GIF87a' || $buf == 'GIF89a' ) ) {
00077                         throw new Exception( "Not a valid GIF file; header: $buf" );
00078                 }
00079 
00080                 // Skip over width and height.
00081                 fread( $fh, 4 );
00082 
00083                 // Read BPP
00084                 $buf = fread( $fh, 1 );
00085                 $bpp = self::decodeBPP( $buf );
00086 
00087                 // Skip over background and aspect ratio
00088                 fread( $fh, 2 );
00089 
00090                 // Skip over the GCT
00091                 self::readGCT( $fh, $bpp );
00092 
00093                 while( !feof( $fh ) ) {
00094                         $buf = fread( $fh, 1 );
00095 
00096                         if ( $buf == self::$gif_frame_sep ) {
00097                                 // Found a frame
00098                                 $frameCount++;
00099 
00100                                 ## Skip bounding box
00101                                 fread( $fh, 8 );
00102 
00103                                 ## Read BPP
00104                                 $buf = fread( $fh, 1 );
00105                                 $bpp = self::decodeBPP( $buf );
00106 
00107                                 ## Read GCT
00108                                 self::readGCT( $fh, $bpp );
00109                                 fread( $fh, 1 );
00110                                 self::skipBlock( $fh );
00111                         } elseif ( $buf == self::$gif_extension_sep ) {
00112                                 $buf = fread( $fh, 1 );
00113                                 if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
00114                                 $extension_code = unpack( 'C', $buf );
00115                                 $extension_code = $extension_code[1];
00116 
00117                                 if ( $extension_code == 0xF9 ) {
00118                                         // Graphics Control Extension.
00119                                         fread( $fh, 1 ); // Block size
00120 
00121                                         fread( $fh, 1 ); // Transparency, disposal method, user input
00122 
00123                                         $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
00124                                         if ( strlen( $buf ) < 2 ) throw new Exception( "Ran out of input" );
00125                                         $delay = unpack( 'v', $buf );
00126                                         $delay = $delay[1];
00127                                         $duration += $delay * 0.01;
00128 
00129                                         fread( $fh, 1 ); // Transparent colour index
00130 
00131                                         $term = fread( $fh, 1 ); // Should be a terminator
00132                                         if ( strlen( $term ) < 1 ) throw new Exception( "Ran out of input" );
00133                                         $term = unpack( 'C', $term );
00134                                         $term = $term[1];
00135                                         if ( $term != 0 ) {
00136                                                 throw new Exception( "Malformed Graphics Control Extension block" );
00137                                         }
00138                                 } elseif ( $extension_code == 0xFE ) {
00139                                         // Comment block(s).
00140                                         $data = self::readBlock( $fh );
00141                                         if ( $data === "" ) {
00142                                                 throw new Exception( 'Read error, zero-length comment block' );
00143                                         }
00144 
00145                                         // The standard says this should be ASCII, however its unclear if
00146                                         // thats true in practise. Check to see if its valid utf-8, if so
00147                                         // assume its that, otherwise assume its windows-1252 (iso-8859-1)
00148                                         $dataCopy = $data;
00149                                         // quickIsNFCVerify has the side effect of replacing any invalid characters
00150                                         UtfNormal::quickIsNFCVerify( $dataCopy );
00151 
00152                                         if ( $dataCopy !== $data ) {
00153                                                 wfSuppressWarnings();
00154                                                 $data = iconv( 'windows-1252', 'UTF-8', $data );
00155                                                 wfRestoreWarnings();
00156                                         }
00157 
00158                                         $commentCount = count( $comment );
00159                                         if ( $commentCount === 0
00160                                                 || $comment[$commentCount-1] !== $data )
00161                                         {
00162                                                 // Some applications repeat the same comment on each
00163                                                 // frame of an animated GIF image, so if this comment
00164                                                 // is identical to the last, only extract once.
00165                                                 $comment[] = $data;
00166                                         }
00167                                 } elseif ( $extension_code == 0xFF ) {
00168                                         // Application extension (Netscape info about the animated gif)
00169                                         // or XMP (or theoretically any other type of extension block)
00170                                         $blockLength = fread( $fh, 1 );
00171                                         if ( strlen( $blockLength ) < 1 ) throw new Exception( "Ran out of input" );
00172                                         $blockLength = unpack( 'C', $blockLength );
00173                                         $blockLength = $blockLength[1];
00174                                         $data = fread( $fh, $blockLength );
00175 
00176                                         if ( $blockLength != 11 ) {
00177                                                 wfDebug( __METHOD__ . ' GIF application block with wrong length' );
00178                                                 fseek( $fh, -($blockLength + 1), SEEK_CUR );
00179                                                 self::skipBlock( $fh );
00180                                                 continue;
00181                                         }
00182 
00183                                         // NETSCAPE2.0 (application name for animated gif)
00184                                         if ( $data == 'NETSCAPE2.0' ) {
00185                                                 $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
00186 
00187                                                 if ( $data != "\x03\x01" ) {
00188                                                         throw new Exception( "Expected \x03\x01, got $data" );
00189                                                 }
00190 
00191                                                 // Unsigned little-endian integer, loop count or zero for "forever"
00192                                                 $loopData = fread( $fh, 2 );
00193                                                 if ( strlen( $loopData ) < 2 ) throw new Exception( "Ran out of input" );
00194                                                 $loopData = unpack( 'v', $loopData );
00195                                                 $loopCount = $loopData[1];
00196 
00197                                                 if ( $loopCount != 1 ) {
00198                                                         $isLooped = true;
00199                                                 }
00200 
00201                                                 // Read out terminator byte
00202                                                 fread( $fh, 1 );
00203                                         } elseif ( $data == 'XMP DataXMP' ) {
00204                                                 // application name for XMP data.
00205                                                 // see pg 18 of XMP spec part 3.
00206 
00207                                                 $xmp = self::readBlock( $fh, true );
00208 
00209                                                 if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
00210                                                         || substr( $xmp, -4 ) !== "\x03\x02\x01\x00" )
00211                                                 {
00212                                                         // this is just a sanity check.
00213                                                         throw new Exception( "XMP does not have magic trailer!" );
00214                                                 }
00215 
00216                                                 // strip out trailer.
00217                                                 $xmp = substr( $xmp, 0, -257 );
00218 
00219                                         } else {
00220                                                 // unrecognized extension block
00221                                                 fseek( $fh, -($blockLength + 1), SEEK_CUR );
00222                                                 self::skipBlock( $fh );
00223                                                 continue;
00224                                         }
00225                                 } else {
00226                                         self::skipBlock( $fh );
00227                                 }
00228                         } elseif ( $buf == self::$gif_term ) {
00229                                 break;
00230                         } else {
00231                                 if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
00232                                 $byte = unpack( 'C', $buf );
00233                                 $byte = $byte[1];
00234                                 throw new Exception( "At position: " . ftell( $fh ) . ", Unknown byte " . $byte );
00235                         }
00236                 }
00237 
00238                 return array(
00239                         'frameCount' => $frameCount,
00240                         'looped' => $isLooped,
00241                         'duration' => $duration,
00242                         'xmp' => $xmp,
00243                         'comment' => $comment,
00244                 );
00245         }
00246 
00252         static function readGCT( $fh, $bpp ) {
00253                 if ( $bpp > 0 ) {
00254                         for( $i = 1; $i <= pow( 2, $bpp ); ++$i ) {
00255                                 fread( $fh, 3 );
00256                         }
00257                 }
00258         }
00259 
00265         static function decodeBPP( $data ) {
00266                 if ( strlen( $data ) < 1 ) throw new Exception( "Ran out of input" );
00267                 $buf = unpack( 'C', $data );
00268                 $buf = $buf[1];
00269                 $bpp = ( $buf & 7 ) + 1;
00270                 $buf >>= 7;
00271 
00272                 $have_map = $buf & 1;
00273 
00274                 return $have_map ? $bpp : 0;
00275         }
00276 
00281         static function skipBlock( $fh ) {
00282                 while ( !feof( $fh ) ) {
00283                         $buf = fread( $fh, 1 );
00284                         if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
00285                         $block_len = unpack( 'C', $buf );
00286                         $block_len = $block_len[1];
00287                         if ( $block_len == 0 ) {
00288                                 return;
00289                         }
00290                         fread( $fh, $block_len );
00291                 }
00292         }
00293 
00308         static function readBlock( $fh, $includeLengths = false ) {
00309                 $data = '';
00310                 $subLength = fread( $fh, 1 );
00311                 $blocks = 0;
00312 
00313                 while( $subLength !== "\0" ) {
00314                         $blocks++;
00315                         if ( $blocks > self::MAX_SUBBLOCKS ) {
00316                                 throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
00317                         }
00318                         if ( feof( $fh ) ) {
00319                                 throw new Exception( "Read error: Unexpected EOF." );
00320                         }
00321                         if ( $includeLengths ) {
00322                                 $data .= $subLength;
00323                         }
00324 
00325                         $data .= fread( $fh, ord( $subLength ) );
00326                         $subLength = fread( $fh, 1 );
00327                 }
00328                 return $data;
00329         }
00330 
00331 }