49 self::$pngSig = pack(
"C8", 137, 80, 78, 71, 13, 10, 26, 10 );
55 'xml:com.adobe.xmp' =>
'xmp',
56 # Artist is unofficial. Author is the recommended
57 # keyword in the PNG spec. However some people output
58 # Artist so support both.
63 'comment' =>
'PNGFileComment',
64 'description' =>
'ImageDescription',
65 'title' =>
'ObjectName',
66 'copyright' =>
'Copyright',
67 # Source as in original device used to make image
68 # not as in who gave you the image
70 'software' =>
'Software',
71 'disclaimer' =>
'Disclaimer',
72 'warning' =>
'ContentWarning',
73 'url' =>
'Identifier', # Not sure
if this is best mapping. Maybe WebStatement.
75 'creation time' =>
'DateTimeDigitized',
86 $colorType =
'unknown';
89 throw new Exception( __METHOD__ .
": No file name specified" );
90 } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
91 throw new Exception( __METHOD__ .
": File $filename does not exist" );
94 $fh = fopen( $filename,
'rb' );
97 throw new Exception( __METHOD__ .
": Unable to open file $filename" );
101 $buf = self::read( $fh, 8 );
102 if ( $buf != self::$pngSig ) {
103 throw new Exception( __METHOD__ .
": Not a valid PNG file; header: $buf" );
107 while ( !feof( $fh ) ) {
108 $buf = self::read( $fh, 4 );
109 $chunk_size = unpack(
"N", $buf )[1];
111 if ( $chunk_size < 0 || $chunk_size > self::MAX_CHUNK_SIZE ) {
112 wfDebug( __METHOD__ .
': Chunk size of ' . $chunk_size .
113 ' too big, skipping. Max size is: ' . self::MAX_CHUNK_SIZE );
114 if ( fseek( $fh, 4 + $chunk_size + self::$crcSize, SEEK_CUR ) !== 0 ) {
115 throw new Exception( __METHOD__ .
': seek error' );
120 $chunk_type = self::read( $fh, 4 );
121 $buf = self::read( $fh, $chunk_size );
122 $crc = self::read( $fh, self::$crcSize );
123 $computed = crc32( $chunk_type . $buf );
124 if ( pack(
'N', $computed ) !== $crc ) {
125 wfDebug( __METHOD__ .
': chunk has invalid CRC, skipping' );
129 if ( $chunk_type ==
"IHDR" ) {
130 $width = unpack(
'N', substr( $buf, 0, 4 ) )[1];
131 $height = unpack(
'N', substr( $buf, 4, 4 ) )[1];
132 $bitDepth = ord( substr( $buf, 8, 1 ) );
135 switch ( ord( substr( $buf, 9, 1 ) ) ) {
137 $colorType =
'greyscale';
140 $colorType =
'truecolour';
143 $colorType =
'index-coloured';
146 $colorType =
'greyscale-alpha';
149 $colorType =
'truecolour-alpha';
152 $colorType =
'unknown';
155 } elseif ( $chunk_type ==
"acTL" ) {
156 if ( $chunk_size < 4 ) {
157 wfDebug( __METHOD__ .
": acTL chunk too small" );
161 $actl = unpack(
"Nframes/Nplays", $buf );
162 $frameCount = $actl[
'frames'];
163 $loopCount = $actl[
'plays'];
164 } elseif ( $chunk_type ==
"fcTL" ) {
165 $buf = substr( $buf, 20 );
166 if ( strlen( $buf ) < 4 ) {
167 wfDebug( __METHOD__ .
": fcTL chunk too small" );
171 $fctldur = unpack(
"ndelay_num/ndelay_den", $buf );
172 if ( $fctldur[
'delay_den'] == 0 ) {
173 $fctldur[
'delay_den'] = 100;
175 if ( $fctldur[
'delay_num'] ) {
176 $duration += $fctldur[
'delay_num'] / $fctldur[
'delay_den'];
178 } elseif ( $chunk_type ==
"iTXt" ) {
182 '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds',
191 $items[1] = strtolower( $items[1] );
192 if ( !isset( self::$textChunks[$items[1]] ) ) {
197 $items[3] = strtolower( $items[3] );
198 if ( $items[3] ==
'' ) {
200 $items[3] =
'x-default';
204 if ( $items[2] ==
"\x01" ) {
205 if ( function_exists(
'gzuncompress' ) && $items[4] ===
"\x00" ) {
206 AtEase::suppressWarnings();
207 $items[5] = gzuncompress( $items[5] );
208 AtEase::restoreWarnings();
210 if ( $items[5] ===
false ) {
212 wfDebug( __METHOD__ .
' Error decompressing iTxt chunk - ' . $items[1] );
216 wfDebug( __METHOD__ .
' Skipping compressed png iTXt chunk due to lack of zlib,'
217 .
" or potentially invalid compression method" );
221 $finalKeyword = self::$textChunks[$items[1]];
222 $text[$finalKeyword][$items[3]] = $items[5];
223 $text[$finalKeyword][
'_type'] =
'lang';
226 wfDebug( __METHOD__ .
": Invalid iTXt chunk" );
228 } elseif ( $chunk_type ==
'tEXt' ) {
230 if ( strpos( $buf,
"\x00" ) ===
false ) {
231 wfDebug( __METHOD__ .
": Invalid tEXt chunk: no null byte" );
235 list( $keyword,
$content ) = explode(
"\x00", $buf, 2 );
236 if ( $keyword ===
'' ) {
237 wfDebug( __METHOD__ .
": Empty tEXt keyword" );
242 $keyword = strtolower( $keyword );
243 if ( !isset( self::$textChunks[$keyword] ) ) {
247 AtEase::suppressWarnings();
249 AtEase::restoreWarnings();
252 wfDebug( __METHOD__ .
": Read error (error with iconv)" );
256 $finalKeyword = self::$textChunks[$keyword];
257 $text[$finalKeyword][
'x-default'] =
$content;
258 $text[$finalKeyword][
'_type'] =
'lang';
259 } elseif ( $chunk_type ==
'zTXt' ) {
260 if ( function_exists(
'gzuncompress' ) ) {
262 if ( strpos( $buf,
"\x00" ) ===
false ) {
263 wfDebug( __METHOD__ .
": No null byte in zTXt chunk" );
267 list( $keyword, $postKeyword ) = explode(
"\x00", $buf, 2 );
268 if ( $keyword ===
'' || $postKeyword ===
'' ) {
269 wfDebug( __METHOD__ .
": Empty zTXt chunk" );
273 $keyword = strtolower( $keyword );
275 if ( !isset( self::$textChunks[$keyword] ) ) {
279 $compression = substr( $postKeyword, 0, 1 );
280 $content = substr( $postKeyword, 1 );
281 if ( $compression !==
"\x00" ) {
282 wfDebug( __METHOD__ .
" Unrecognized compression method in zTXt ($keyword). Skipping." );
286 AtEase::suppressWarnings();
288 AtEase::restoreWarnings();
292 wfDebug( __METHOD__ .
' Error decompressing zTXt chunk - ' . $keyword );
296 AtEase::suppressWarnings();
298 AtEase::restoreWarnings();
301 wfDebug( __METHOD__ .
": iconv error in zTXt chunk" );
305 $finalKeyword = self::$textChunks[$keyword];
306 $text[$finalKeyword][
'x-default'] =
$content;
307 $text[$finalKeyword][
'_type'] =
'lang';
309 wfDebug( __METHOD__ .
" Cannot decompress zTXt chunk due to lack of zlib. Skipping." );
311 } elseif ( $chunk_type ==
'tIME' ) {
313 if ( $chunk_size !== 7 ) {
314 wfDebug( __METHOD__ .
": tIME wrong size" );
319 $t = unpack(
"ny/Cm/Cd/Ch/Cmin/Cs", $buf );
320 $strTime = sprintf(
"%04d%02d%02d%02d%02d%02d",
322 $t[
'min'],
$t[
's'] );
327 $text[
'DateTime'] = $exifTime;
329 } elseif ( $chunk_type ==
'pHYs' ) {
331 if ( $chunk_size !== 9 ) {
332 wfDebug( __METHOD__ .
": pHYs wrong size" );
336 $dim = unpack(
"Nwidth/Nheight/Cunit", $buf );
337 if ( $dim[
'unit'] == 1 ) {
340 if ( $dim[
'width'] > 0 && $dim[
'height'] > 0 ) {
343 $text[
'XResolution'] = $dim[
'width']
345 $text[
'YResolution'] = $dim[
'height']
347 $text[
'ResolutionUnit'] = 3;
351 } elseif ( $chunk_type ==
"IEND" ) {
357 if ( $loopCount > 1 ) {
358 $duration *= $loopCount;
361 if ( isset( $text[
'DateTimeDigitized'] ) ) {
363 foreach ( $text[
'DateTimeDigitized'] as $name => &$value ) {
364 if ( $name ===
'_type' ) {
391 'frameCount' => $frameCount,
392 'loopCount' => $loopCount,
393 'duration' => $duration,
395 'bitDepth' => $bitDepth,
396 'colorType' => $colorType,