MediaWiki  1.28.0
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  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 
56  static function getMetadata( $filename ) {
57  self::$gifFrameSep = pack( "C", ord( "," ) );
58  self::$gifExtensionSep = pack( "C", ord( "!" ) );
59  self::$gifTerm = pack( "C", ord( ";" ) );
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  // Skip over width and height.
86  fread( $fh, 4 );
87 
88  // Read BPP
89  $buf = fread( $fh, 1 );
90  $bpp = self::decodeBPP( $buf );
91 
92  // Skip over background and aspect ratio
93  fread( $fh, 2 );
94 
95  // Skip over the GCT
96  self::readGCT( $fh, $bpp );
97 
98  while ( !feof( $fh ) ) {
99  $buf = fread( $fh, 1 );
100 
101  if ( $buf == self::$gifFrameSep ) {
102  // Found a frame
103  $frameCount++;
104 
105  # # Skip bounding box
106  fread( $fh, 8 );
107 
108  # # Read BPP
109  $buf = fread( $fh, 1 );
110  $bpp = self::decodeBPP( $buf );
111 
112  # # Read GCT
113  self::readGCT( $fh, $bpp );
114  fread( $fh, 1 );
115  self::skipBlock( $fh );
116  } elseif ( $buf == self::$gifExtensionSep ) {
117  $buf = fread( $fh, 1 );
118  if ( strlen( $buf ) < 1 ) {
119  throw new Exception( "Ran out of input" );
120  }
121  $extension_code = unpack( 'C', $buf )[1];
122 
123  if ( $extension_code == 0xF9 ) {
124  // Graphics Control Extension.
125  fread( $fh, 1 ); // Block size
126 
127  fread( $fh, 1 ); // Transparency, disposal method, user input
128 
129  $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
130  if ( strlen( $buf ) < 2 ) {
131  throw new Exception( "Ran out of input" );
132  }
133  $delay = unpack( 'v', $buf )[1];
134  $duration += $delay * 0.01;
135 
136  fread( $fh, 1 ); // Transparent colour index
137 
138  $term = fread( $fh, 1 ); // Should be a terminator
139  if ( strlen( $term ) < 1 ) {
140  throw new Exception( "Ran out of input" );
141  }
142  $term = unpack( 'C', $term )[1];
143  if ( $term != 0 ) {
144  throw new Exception( "Malformed Graphics Control Extension block" );
145  }
146  } elseif ( $extension_code == 0xFE ) {
147  // Comment block(s).
148  $data = self::readBlock( $fh );
149  if ( $data === "" ) {
150  throw new Exception( 'Read error, zero-length comment block' );
151  }
152 
153  // The standard says this should be ASCII, however its unclear if
154  // thats true in practise. Check to see if its valid utf-8, if so
155  // assume its that, otherwise assume its windows-1252 (iso-8859-1)
156  $dataCopy = $data;
157  // quickIsNFCVerify has the side effect of replacing any invalid characters
158  UtfNormal\Validator::quickIsNFCVerify( $dataCopy );
159 
160  if ( $dataCopy !== $data ) {
161  MediaWiki\suppressWarnings();
162  $data = iconv( 'windows-1252', 'UTF-8', $data );
163  MediaWiki\restoreWarnings();
164  }
165 
166  $commentCount = count( $comment );
167  if ( $commentCount === 0
168  || $comment[$commentCount - 1] !== $data
169  ) {
170  // Some applications repeat the same comment on each
171  // frame of an animated GIF image, so if this comment
172  // is identical to the last, only extract once.
173  $comment[] = $data;
174  }
175  } elseif ( $extension_code == 0xFF ) {
176  // Application extension (Netscape info about the animated gif)
177  // or XMP (or theoretically any other type of extension block)
178  $blockLength = fread( $fh, 1 );
179  if ( strlen( $blockLength ) < 1 ) {
180  throw new Exception( "Ran out of input" );
181  }
182  $blockLength = unpack( 'C', $blockLength )[1];
183  $data = fread( $fh, $blockLength );
184 
185  if ( $blockLength != 11 ) {
186  wfDebug( __METHOD__ . " GIF application block with wrong length\n" );
187  fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
188  self::skipBlock( $fh );
189  continue;
190  }
191 
192  // NETSCAPE2.0 (application name for animated gif)
193  if ( $data == 'NETSCAPE2.0' ) {
194  $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
195 
196  if ( $data != "\x03\x01" ) {
197  throw new Exception( "Expected \x03\x01, got $data" );
198  }
199 
200  // Unsigned little-endian integer, loop count or zero for "forever"
201  $loopData = fread( $fh, 2 );
202  if ( strlen( $loopData ) < 2 ) {
203  throw new Exception( "Ran out of input" );
204  }
205  $loopCount = unpack( 'v', $loopData )[1];
206 
207  if ( $loopCount != 1 ) {
208  $isLooped = true;
209  }
210 
211  // Read out terminator byte
212  fread( $fh, 1 );
213  } elseif ( $data == 'XMP DataXMP' ) {
214  // application name for XMP data.
215  // see pg 18 of XMP spec part 3.
216 
217  $xmp = self::readBlock( $fh, true );
218 
219  if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
220  || substr( $xmp, -4 ) !== "\x03\x02\x01\x00"
221  ) {
222  // this is just a sanity check.
223  throw new Exception( "XMP does not have magic trailer!" );
224  }
225 
226  // strip out trailer.
227  $xmp = substr( $xmp, 0, -257 );
228  } else {
229  // unrecognized extension block
230  fseek( $fh, -( $blockLength + 1 ), SEEK_CUR );
231  self::skipBlock( $fh );
232  continue;
233  }
234  } else {
235  self::skipBlock( $fh );
236  }
237  } elseif ( $buf == self::$gifTerm ) {
238  break;
239  } else {
240  if ( strlen( $buf ) < 1 ) {
241  throw new Exception( "Ran out of input" );
242  }
243  $byte = unpack( 'C', $buf )[1];
244  throw new Exception( "At position: " . ftell( $fh ) . ", Unknown byte " . $byte );
245  }
246  }
247 
248  return [
249  'frameCount' => $frameCount,
250  'looped' => $isLooped,
251  'duration' => $duration,
252  'xmp' => $xmp,
253  'comment' => $comment,
254  ];
255  }
256 
262  static function readGCT( $fh, $bpp ) {
263  if ( $bpp > 0 ) {
264  $max = pow( 2, $bpp );
265  for ( $i = 1; $i <= $max; ++$i ) {
266  fread( $fh, 3 );
267  }
268  }
269  }
270 
276  static function decodeBPP( $data ) {
277  if ( strlen( $data ) < 1 ) {
278  throw new Exception( "Ran out of input" );
279  }
280  $buf = unpack( 'C', $data )[1];
281  $bpp = ( $buf & 7 ) + 1;
282  $buf >>= 7;
283 
284  $have_map = $buf & 1;
285 
286  return $have_map ? $bpp : 0;
287  }
288 
293  static function skipBlock( $fh ) {
294  while ( !feof( $fh ) ) {
295  $buf = fread( $fh, 1 );
296  if ( strlen( $buf ) < 1 ) {
297  throw new Exception( "Ran out of input" );
298  }
299  $block_len = unpack( 'C', $buf )[1];
300  if ( $block_len == 0 ) {
301  return;
302  }
303  fread( $fh, $block_len );
304  }
305  }
306 
321  static function readBlock( $fh, $includeLengths = false ) {
322  $data = '';
323  $subLength = fread( $fh, 1 );
324  $blocks = 0;
325 
326  while ( $subLength !== "\0" ) {
327  $blocks++;
328  if ( $blocks > self::MAX_SUBBLOCKS ) {
329  throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
330  }
331  if ( feof( $fh ) ) {
332  throw new Exception( "Read error: Unexpected EOF." );
333  }
334  if ( $includeLengths ) {
335  $data .= $subLength;
336  }
337 
338  $data .= fread( $fh, ord( $subLength ) );
339  $subLength = fread( $fh, 1 );
340  }
341 
342  return $data;
343  }
344 }
external whereas SearchGetNearMatch runs after $term
Definition: hooks.txt:2713
$comment
static readBlock($fh, $includeLengths=false)
Read a block.
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static getMetadata($filename)
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35