MediaWiki  master
GIFMetadataExtractor.php
Go to the documentation of this file.
1 <?php
36  private static $gifFrameSep;
37 
39  private static $gifExtensionSep;
40 
42  private static $gifTerm;
43 
44  public 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  private const MAX_SUBBLOCKS = 262144; // 5 MiB divided by 20.
50 
56  public 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  list( $bpp, $have_map ) = self::decodeBPP( $buf );
94 
95  // Skip over background and aspect ratio
96  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
97  fread( $fh, 2 );
98 
99  // Skip over the GCT
100  if ( $have_map ) {
101  self::readGCT( $fh, $bpp );
102  }
103 
104  while ( !feof( $fh ) ) {
105  $buf = fread( $fh, 1 );
106 
107  if ( $buf == self::$gifFrameSep ) {
108  // Found a frame
109  $frameCount++;
110 
111  # # Skip bounding box
112  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
113  fread( $fh, 8 );
114 
115  # # Read BPP
116  $buf = fread( $fh, 1 );
117  list( $bpp, $have_map ) = self::decodeBPP( $buf );
118 
119  # # Read GCT
120  if ( $have_map ) {
121  self::readGCT( $fh, $bpp );
122  }
123  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
124  fread( $fh, 1 );
125  self::skipBlock( $fh );
126  } elseif ( $buf == self::$gifExtensionSep ) {
127  $buf = fread( $fh, 1 );
128  if ( strlen( $buf ) < 1 ) {
129  throw new Exception( "Ran out of input" );
130  }
131  $extension_code = unpack( 'C', $buf )[1];
132 
133  if ( $extension_code == 0xF9 ) {
134  // Graphics Control Extension.
135  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
136  fread( $fh, 1 ); // Block size
137 
138  // @phan-suppress-next-next-line PhanPluginUseReturnValueInternalKnown
139  // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
140  fread( $fh, 1 ); // Transparency, disposal method, user input
141 
142  $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
143  if ( strlen( $buf ) < 2 ) {
144  throw new Exception( "Ran out of input" );
145  }
146  $delay = unpack( 'v', $buf )[1];
147  $duration += $delay * 0.01;
148 
149  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
150  fread( $fh, 1 ); // Transparent colour index
151 
152  $term = fread( $fh, 1 ); // Should be a terminator
153  if ( strlen( $term ) < 1 ) {
154  throw new Exception( "Ran out of input" );
155  }
156  $term = unpack( 'C', $term )[1];
157  if ( $term != 0 ) {
158  throw new Exception( "Malformed Graphics Control Extension block" );
159  }
160  } elseif ( $extension_code == 0xFE ) {
161  // Comment block(s).
162  $data = self::readBlock( $fh );
163  if ( $data === "" ) {
164  throw new Exception( 'Read error, zero-length comment block' );
165  }
166 
167  // The standard says this should be ASCII, however its unclear if
168  // thats true in practise. Check to see if its valid utf-8, if so
169  // assume its that, otherwise assume its windows-1252 (iso-8859-1)
170  $dataCopy = $data;
171  // quickIsNFCVerify has the side effect of replacing any invalid characters
172  UtfNormal\Validator::quickIsNFCVerify( $dataCopy );
173 
174  if ( $dataCopy !== $data ) {
175  Wikimedia\suppressWarnings();
176  $data = iconv( 'windows-1252', 'UTF-8', $data );
177  Wikimedia\restoreWarnings();
178  }
179 
180  $commentCount = count( $comment );
181  if ( $commentCount === 0
182  // @phan-suppress-next-line PhanTypeInvalidDimOffset
183  || $comment[$commentCount - 1] !== $data
184  ) {
185  // Some applications repeat the same comment on each
186  // frame of an animated GIF image, so if this comment
187  // is identical to the last, only extract once.
188  $comment[] = $data;
189  }
190  } elseif ( $extension_code == 0xFF ) {
191  // Application extension (Netscape info about the animated gif)
192  // or XMP (or theoretically any other type of extension block)
193  $blockLength = fread( $fh, 1 );
194  if ( strlen( $blockLength ) < 1 ) {
195  throw new Exception( "Ran out of input" );
196  }
197  $blockLength = unpack( 'C', $blockLength )[1];
198  $data = fread( $fh, $blockLength );
199 
200  if ( $blockLength != 11 ) {
201  wfDebug( __METHOD__ . " GIF application block with wrong length" );
202  fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
203  self::skipBlock( $fh );
204  continue;
205  }
206 
207  // NETSCAPE2.0 (application name for animated gif)
208  if ( $data == 'NETSCAPE2.0' ) {
209  $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
210 
211  if ( $data != "\x03\x01" ) {
212  throw new Exception( "Expected \x03\x01, got $data" );
213  }
214 
215  // Unsigned little-endian integer, loop count or zero for "forever"
216  $loopData = fread( $fh, 2 );
217  if ( strlen( $loopData ) < 2 ) {
218  throw new Exception( "Ran out of input" );
219  }
220  $loopCount = unpack( 'v', $loopData )[1];
221 
222  if ( $loopCount != 1 ) {
223  $isLooped = true;
224  }
225 
226  // Read out terminator byte
227  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
228  fread( $fh, 1 );
229  } elseif ( $data == 'XMP DataXMP' ) {
230  // application name for XMP data.
231  // see pg 18 of XMP spec part 3.
232 
233  $xmp = self::readBlock( $fh, true );
234 
235  if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
236  || substr( $xmp, -4 ) !== "\x03\x02\x01\x00"
237  ) {
238  throw new Exception( "XMP does not have magic trailer!" );
239  }
240 
241  // strip out trailer.
242  $xmp = substr( $xmp, 0, -257 );
243  } else {
244  // unrecognized extension block
245  fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
246  self::skipBlock( $fh );
247  }
248  } else {
249  self::skipBlock( $fh );
250  }
251  } elseif ( $buf == self::$gifTerm ) {
252  break;
253  } else {
254  if ( strlen( $buf ) < 1 ) {
255  throw new Exception( "Ran out of input" );
256  }
257  $byte = unpack( 'C', $buf )[1];
258  throw new Exception( "At position: " . ftell( $fh ) . ", Unknown byte " . $byte );
259  }
260  }
261 
262  return [
263  'frameCount' => $frameCount,
264  'looped' => $isLooped,
265  'duration' => $duration,
266  'xmp' => $xmp,
267  'comment' => $comment,
268  'width' => $width,
269  'height' => $height,
270  'bits' => $bpp,
271  ];
272  }
273 
279  private static function readGCT( $fh, $bpp ) {
280  $max = 2 ** $bpp;
281  for ( $i = 1; $i <= $max; ++$i ) {
282  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
283  fread( $fh, 3 );
284  }
285  }
286 
292  private static function decodeBPP( $data ) {
293  if ( strlen( $data ) < 1 ) {
294  throw new Exception( "Ran out of input" );
295  }
296  $buf = unpack( 'C', $data )[1];
297  $bpp = ( $buf & 7 ) + 1;
298  $buf >>= 7;
299 
300  $have_map = $buf & 1;
301 
302  return [ $bpp, $have_map ];
303  }
304 
309  private static function skipBlock( $fh ) {
310  while ( !feof( $fh ) ) {
311  $buf = fread( $fh, 1 );
312  if ( strlen( $buf ) < 1 ) {
313  throw new Exception( "Ran out of input" );
314  }
315  $block_len = unpack( 'C', $buf )[1];
316  if ( $block_len == 0 ) {
317  return;
318  }
319  // @phan-suppress-next-line PhanPluginUseReturnValueInternalKnown
320  fread( $fh, $block_len );
321  }
322  }
323 
338  private static function readBlock( $fh, $includeLengths = false ) {
339  $data = '';
340  $subLength = fread( $fh, 1 );
341  $blocks = 0;
342 
343  while ( $subLength !== "\0" ) {
344  $blocks++;
345  if ( $blocks > self::MAX_SUBBLOCKS ) {
346  throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
347  }
348  if ( feof( $fh ) ) {
349  throw new Exception( "Read error: Unexpected EOF." );
350  }
351  if ( $includeLengths ) {
352  $data .= $subLength;
353  }
354 
355  $data .= fread( $fh, ord( $subLength ) );
356  $subLength = fread( $fh, 1 );
357  }
358 
359  return $data;
360  }
361 }
GIFMetadataExtractor
GIF frame counter.
Definition: GIFMetadataExtractor.php:34
GIFMetadataExtractor\MAX_SUBBLOCKS
const MAX_SUBBLOCKS
Definition: GIFMetadataExtractor.php:49
GIFMetadataExtractor\$gifFrameSep
static string $gifFrameSep
Definition: GIFMetadataExtractor.php:36
GIFMetadataExtractor\$gifTerm
static string $gifTerm
Definition: GIFMetadataExtractor.php:42
GIFMetadataExtractor\$gifExtensionSep
static string $gifExtensionSep
Definition: GIFMetadataExtractor.php:39
GIFMetadataExtractor\getMetadata
static getMetadata( $filename)
Definition: GIFMetadataExtractor.php:56
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
GIFMetadataExtractor\readBlock
static readBlock( $fh, $includeLengths=false)
Read a block.
Definition: GIFMetadataExtractor.php:338
GIFMetadataExtractor\decodeBPP
static decodeBPP( $data)
Definition: GIFMetadataExtractor.php:292
GIFMetadataExtractor\readGCT
static readGCT( $fh, $bpp)
Definition: GIFMetadataExtractor.php:279
GIFMetadataExtractor\skipBlock
static skipBlock( $fh)
Definition: GIFMetadataExtractor.php:309
GIFMetadataExtractor\VERSION
const VERSION
Definition: GIFMetadataExtractor.php:44