MediaWiki  REL1_20
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                                         
00186                                                 $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
00187 
00188                                                 if ($data != "\x03\x01") {
00189                                                         throw new Exception( "Expected \x03\x01, got $data" );
00190                                                 }
00191                                                 
00192                                                 // Unsigned little-endian integer, loop count or zero for "forever"
00193                                                 $loopData = fread( $fh, 2 );
00194                                                 if ( strlen( $loopData ) < 2 ) throw new Exception( "Ran out of input" );
00195                                                 $loopData = unpack( 'v', $loopData );
00196                                                 $loopCount = $loopData[1];
00197                                                 
00198                                                 if ($loopCount != 1) {
00199                                                         $isLooped = true;
00200                                                 }
00201                                                 
00202                                                 // Read out terminator byte
00203                                                 fread( $fh, 1 );
00204                                         } elseif ( $data == 'XMP DataXMP' ) {
00205                                                 // application name for XMP data.
00206                                                 // see pg 18 of XMP spec part 3.
00207 
00208                                                 $xmp = self::readBlock( $fh, true );
00209 
00210                                                 if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
00211                                                         || substr( $xmp, -4 ) !== "\x03\x02\x01\x00" )
00212                                                 {
00213                                                         // this is just a sanity check.
00214                                                         throw new Exception( "XMP does not have magic trailer!" );
00215                                                 }
00216 
00217                                                 // strip out trailer.
00218                                                 $xmp = substr( $xmp, 0, -257 );
00219 
00220                                         } else {
00221                                                 // unrecognized extension block
00222                                                 fseek( $fh, -($blockLength + 1), SEEK_CUR );
00223                                                 self::skipBlock( $fh );
00224                                                 continue;
00225                                         }
00226                                 } else {
00227                                         self::skipBlock( $fh );
00228                                 }
00229                         } elseif ( $buf == self::$gif_term ) {
00230                                 break;
00231                         } else {
00232                                 if ( strlen( $buf ) < 1 ) throw new Exception( "Ran out of input" );
00233                                 $byte = unpack( 'C', $buf );
00234                                 $byte = $byte[1];
00235                                 throw new Exception( "At position: ".ftell($fh). ", Unknown byte ".$byte );
00236                         }
00237                 }
00238 
00239                 return array(
00240                         'frameCount' => $frameCount,
00241                         'looped' => $isLooped,
00242                         'duration' => $duration,
00243                         'xmp' => $xmp,
00244                         'comment' => $comment,
00245                 );
00246         }
00247 
00253         static function readGCT( $fh, $bpp ) {
00254                 if ( $bpp > 0 ) {
00255                         for( $i=1; $i<=pow( 2, $bpp ); ++$i ) {
00256                                 fread( $fh, 3 );
00257                         }
00258                 }
00259         }
00260 
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         }
00306         static function readBlock( $fh, $includeLengths = false ) {
00307                 $data = '';
00308                 $subLength = fread( $fh, 1 );
00309                 $blocks = 0;
00310 
00311                 while( $subLength !== "\0" ) {
00312                         $blocks++;
00313                         if ( $blocks > self::MAX_SUBBLOCKS ) {
00314                                 throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
00315                         }
00316                         if ( feof( $fh ) ) {
00317                                 throw new Exception( "Read error: Unexpected EOF." );
00318                         }
00319                         if ( $includeLengths ) {
00320                                 $data .= $subLength;
00321                         }
00322 
00323                         $data .= fread( $fh, ord( $subLength ) );
00324                         $subLength = fread( $fh, 1 );
00325                 }
00326                 return $data;
00327         }
00328 
00329 }