]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/media/GIFMetadataExtractor.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / media / GIFMetadataExtractor.php
1 <?php
2 /**
3  * GIF frame counter.
4  *
5  * Originally written in Perl by Steve Sanbeg.
6  * Ported to PHP by Andrew Garrett
7  * Deliberately not using MWExceptions to avoid external dependencies, encouraging
8  * redistribution.
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License along
21  * with this program; if not, write to the Free Software Foundation, Inc.,
22  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23  * http://www.gnu.org/copyleft/gpl.html
24  *
25  * @file
26  * @ingroup Media
27  */
28
29 /**
30  * GIF frame counter.
31  *
32  * @ingroup Media
33  */
34 class GIFMetadataExtractor {
35         /** @var string */
36         private static $gifFrameSep;
37
38         /** @var string */
39         private static $gifExtensionSep;
40
41         /** @var string */
42         private static $gifTerm;
43
44         const VERSION = 1;
45
46         // Each sub-block is less than or equal to 255 bytes.
47         // Most of the time its 255 bytes, except for in XMP
48         // blocks, where it's usually between 32-127 bytes each.
49         const MAX_SUBBLOCKS = 262144; // 5mb divided by 20.
50
51         /**
52          * @throws Exception
53          * @param string $filename
54          * @return array
55          */
56         static function getMetadata( $filename ) {
57                 self::$gifFrameSep = pack( "C", ord( "," ) ); // 2C
58                 self::$gifExtensionSep = pack( "C", ord( "!" ) ); // 21
59                 self::$gifTerm = pack( "C", ord( ";" ) ); // 3B
60
61                 $frameCount = 0;
62                 $duration = 0.0;
63                 $isLooped = false;
64                 $xmp = "";
65                 $comment = [];
66
67                 if ( !$filename ) {
68                         throw new Exception( "No file name specified" );
69                 } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
70                         throw new Exception( "File $filename does not exist" );
71                 }
72
73                 $fh = fopen( $filename, 'rb' );
74
75                 if ( !$fh ) {
76                         throw new Exception( "Unable to open file $filename" );
77                 }
78
79                 // Check for the GIF header
80                 $buf = fread( $fh, 6 );
81                 if ( !( $buf == 'GIF87a' || $buf == 'GIF89a' ) ) {
82                         throw new Exception( "Not a valid GIF file; header: $buf" );
83                 }
84
85                 // Read width and height.
86                 $buf = fread( $fh, 2 );
87                 $width = unpack( 'v', $buf )[1];
88                 $buf = fread( $fh, 2 );
89                 $height = unpack( 'v', $buf )[1];
90
91                 // Read BPP
92                 $buf = fread( $fh, 1 );
93                 $bpp = self::decodeBPP( $buf );
94
95                 // Skip over background and aspect ratio
96                 fread( $fh, 2 );
97
98                 // Skip over the GCT
99                 self::readGCT( $fh, $bpp );
100
101                 while ( !feof( $fh ) ) {
102                         $buf = fread( $fh, 1 );
103
104                         if ( $buf == self::$gifFrameSep ) {
105                                 // Found a frame
106                                 $frameCount++;
107
108                                 # # Skip bounding box
109                                 fread( $fh, 8 );
110
111                                 # # Read BPP
112                                 $buf = fread( $fh, 1 );
113                                 $bpp = self::decodeBPP( $buf );
114
115                                 # # Read GCT
116                                 self::readGCT( $fh, $bpp );
117                                 fread( $fh, 1 );
118                                 self::skipBlock( $fh );
119                         } elseif ( $buf == self::$gifExtensionSep ) {
120                                 $buf = fread( $fh, 1 );
121                                 if ( strlen( $buf ) < 1 ) {
122                                         throw new Exception( "Ran out of input" );
123                                 }
124                                 $extension_code = unpack( 'C', $buf )[1];
125
126                                 if ( $extension_code == 0xF9 ) {
127                                         // Graphics Control Extension.
128                                         fread( $fh, 1 ); // Block size
129
130                                         fread( $fh, 1 ); // Transparency, disposal method, user input
131
132                                         $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
133                                         if ( strlen( $buf ) < 2 ) {
134                                                 throw new Exception( "Ran out of input" );
135                                         }
136                                         $delay = unpack( 'v', $buf )[1];
137                                         $duration += $delay * 0.01;
138
139                                         fread( $fh, 1 ); // Transparent colour index
140
141                                         $term = fread( $fh, 1 ); // Should be a terminator
142                                         if ( strlen( $term ) < 1 ) {
143                                                 throw new Exception( "Ran out of input" );
144                                         }
145                                         $term = unpack( 'C', $term )[1];
146                                         if ( $term != 0 ) {
147                                                 throw new Exception( "Malformed Graphics Control Extension block" );
148                                         }
149                                 } elseif ( $extension_code == 0xFE ) {
150                                         // Comment block(s).
151                                         $data = self::readBlock( $fh );
152                                         if ( $data === "" ) {
153                                                 throw new Exception( 'Read error, zero-length comment block' );
154                                         }
155
156                                         // The standard says this should be ASCII, however its unclear if
157                                         // thats true in practise. Check to see if its valid utf-8, if so
158                                         // assume its that, otherwise assume its windows-1252 (iso-8859-1)
159                                         $dataCopy = $data;
160                                         // quickIsNFCVerify has the side effect of replacing any invalid characters
161                                         UtfNormal\Validator::quickIsNFCVerify( $dataCopy );
162
163                                         if ( $dataCopy !== $data ) {
164                                                 MediaWiki\suppressWarnings();
165                                                 $data = iconv( 'windows-1252', 'UTF-8', $data );
166                                                 MediaWiki\restoreWarnings();
167                                         }
168
169                                         $commentCount = count( $comment );
170                                         if ( $commentCount === 0
171                                                 || $comment[$commentCount - 1] !== $data
172                                         ) {
173                                                 // Some applications repeat the same comment on each
174                                                 // frame of an animated GIF image, so if this comment
175                                                 // is identical to the last, only extract once.
176                                                 $comment[] = $data;
177                                         }
178                                 } elseif ( $extension_code == 0xFF ) {
179                                         // Application extension (Netscape info about the animated gif)
180                                         // or XMP (or theoretically any other type of extension block)
181                                         $blockLength = fread( $fh, 1 );
182                                         if ( strlen( $blockLength ) < 1 ) {
183                                                 throw new Exception( "Ran out of input" );
184                                         }
185                                         $blockLength = unpack( 'C', $blockLength )[1];
186                                         $data = fread( $fh, $blockLength );
187
188                                         if ( $blockLength != 11 ) {
189                                                 wfDebug( __METHOD__ . " GIF application block with wrong length\n" );
190                                                 fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
191                                                 self::skipBlock( $fh );
192                                                 continue;
193                                         }
194
195                                         // NETSCAPE2.0 (application name for animated gif)
196                                         if ( $data == 'NETSCAPE2.0' ) {
197                                                 $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
198
199                                                 if ( $data != "\x03\x01" ) {
200                                                         throw new Exception( "Expected \x03\x01, got $data" );
201                                                 }
202
203                                                 // Unsigned little-endian integer, loop count or zero for "forever"
204                                                 $loopData = fread( $fh, 2 );
205                                                 if ( strlen( $loopData ) < 2 ) {
206                                                         throw new Exception( "Ran out of input" );
207                                                 }
208                                                 $loopCount = unpack( 'v', $loopData )[1];
209
210                                                 if ( $loopCount != 1 ) {
211                                                         $isLooped = true;
212                                                 }
213
214                                                 // Read out terminator byte
215                                                 fread( $fh, 1 );
216                                         } elseif ( $data == 'XMP DataXMP' ) {
217                                                 // application name for XMP data.
218                                                 // see pg 18 of XMP spec part 3.
219
220                                                 $xmp = self::readBlock( $fh, true );
221
222                                                 if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
223                                                         || substr( $xmp, -4 ) !== "\x03\x02\x01\x00"
224                                                 ) {
225                                                         // this is just a sanity check.
226                                                         throw new Exception( "XMP does not have magic trailer!" );
227                                                 }
228
229                                                 // strip out trailer.
230                                                 $xmp = substr( $xmp, 0, -257 );
231                                         } else {
232                                                 // unrecognized extension block
233                                                 fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
234                                                 self::skipBlock( $fh );
235                                                 continue;
236                                         }
237                                 } else {
238                                         self::skipBlock( $fh );
239                                 }
240                         } elseif ( $buf == self::$gifTerm ) {
241                                 break;
242                         } else {
243                                 if ( strlen( $buf ) < 1 ) {
244                                         throw new Exception( "Ran out of input" );
245                                 }
246                                 $byte = unpack( 'C', $buf )[1];
247                                 throw new Exception( "At position: " . ftell( $fh ) . ", Unknown byte " . $byte );
248                         }
249                 }
250
251                 return [
252                         'frameCount' => $frameCount,
253                         'looped' => $isLooped,
254                         'duration' => $duration,
255                         'xmp' => $xmp,
256                         'comment' => $comment,
257                 ];
258         }
259
260         /**
261          * @param resource $fh
262          * @param int $bpp
263          * @return void
264          */
265         static function readGCT( $fh, $bpp ) {
266                 if ( $bpp > 0 ) {
267                         $max = pow( 2, $bpp );
268                         for ( $i = 1; $i <= $max; ++$i ) {
269                                 fread( $fh, 3 );
270                         }
271                 }
272         }
273
274         /**
275          * @param string $data
276          * @throws Exception
277          * @return int
278          */
279         static function decodeBPP( $data ) {
280                 if ( strlen( $data ) < 1 ) {
281                         throw new Exception( "Ran out of input" );
282                 }
283                 $buf = unpack( 'C', $data )[1];
284                 $bpp = ( $buf & 7 ) + 1;
285                 $buf >>= 7;
286
287                 $have_map = $buf & 1;
288
289                 return $have_map ? $bpp : 0;
290         }
291
292         /**
293          * @param resource $fh
294          * @throws Exception
295          */
296         static function skipBlock( $fh ) {
297                 while ( !feof( $fh ) ) {
298                         $buf = fread( $fh, 1 );
299                         if ( strlen( $buf ) < 1 ) {
300                                 throw new Exception( "Ran out of input" );
301                         }
302                         $block_len = unpack( 'C', $buf )[1];
303                         if ( $block_len == 0 ) {
304                                 return;
305                         }
306                         fread( $fh, $block_len );
307                 }
308         }
309
310         /**
311          * Read a block. In the GIF format, a block is made up of
312          * several sub-blocks. Each sub block starts with one byte
313          * saying how long the sub-block is, followed by the sub-block.
314          * The entire block is terminated by a sub-block of length
315          * 0.
316          * @param resource $fh File handle
317          * @param bool $includeLengths Include the length bytes of the
318          *  sub-blocks in the returned value. Normally this is false,
319          *  except XMP is weird and does a hack where you need to keep
320          *  these length bytes.
321          * @throws Exception
322          * @return string The data.
323          */
324         static function readBlock( $fh, $includeLengths = false ) {
325                 $data = '';
326                 $subLength = fread( $fh, 1 );
327                 $blocks = 0;
328
329                 while ( $subLength !== "\0" ) {
330                         $blocks++;
331                         if ( $blocks > self::MAX_SUBBLOCKS ) {
332                                 throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
333                         }
334                         if ( feof( $fh ) ) {
335                                 throw new Exception( "Read error: Unexpected EOF." );
336                         }
337                         if ( $includeLengths ) {
338                                 $data .= $subLength;
339                         }
340
341                         $data .= fread( $fh, ord( $subLength ) );
342                         $subLength = fread( $fh, 1 );
343                 }
344
345                 return $data;
346         }
347 }