MediaWiki
REL1_19
|
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 }