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