45 'xml:com.adobe.xmp' =>
'xmp',
46 # Artist is unofficial. Author is the recommended
47 # keyword in the PNG spec. However some people output
48 # Artist so support both.
53 'comment' =>
'PNGFileComment',
54 'description' =>
'ImageDescription',
55 'title' =>
'ObjectName',
56 'copyright' =>
'Copyright',
57 # Source as in original device used to make image
58 # not as in who gave you the image
60 'software' =>
'Software',
61 'disclaimer' =>
'Disclaimer',
62 'warning' =>
'ContentWarning',
63 'url' =>
'Identifier', # Not sure
if this is best mapping. Maybe WebStatement.
65 'creation time' =>
'DateTimeDigitized',
76 $colorType =
'unknown';
80 throw new InvalidArgumentException( __METHOD__ .
": No file name specified" );
83 if ( !file_exists( $filename ) || is_dir( $filename ) ) {
84 throw new InvalidArgumentException( __METHOD__ .
": File $filename does not exist" );
87 $fh = fopen( $filename,
'rb' );
90 throw new InvalidArgumentException( __METHOD__ .
": Unable to open file $filename" );
94 $buf = self::read( $fh, 8 );
95 if ( $buf !==
"\x89PNG\x0d\x0a\x1a\x0a" ) {
96 throw new InvalidArgumentException( __METHOD__ .
": Not a valid PNG file; header: $buf" );
100 while ( !feof( $fh ) ) {
101 $buf = self::read( $fh, 4 );
102 $chunk_size = unpack(
"N", $buf )[1];
104 if ( $chunk_size < 0 || $chunk_size > self::MAX_CHUNK_SIZE ) {
105 wfDebug( __METHOD__ .
': Chunk size of ' . $chunk_size .
106 ' too big, skipping. Max size is: ' . self::MAX_CHUNK_SIZE );
107 if ( fseek( $fh, 4 + $chunk_size + self::$crcSize, SEEK_CUR ) !== 0 ) {
108 throw new InvalidArgumentException( __METHOD__ .
': seek error' );
113 $chunk_type = self::read( $fh, 4 );
114 $buf = self::read( $fh, $chunk_size );
115 $crc = self::read( $fh, self::$crcSize );
116 $computed = crc32( $chunk_type . $buf );
117 if ( pack(
'N', $computed ) !== $crc ) {
118 wfDebug( __METHOD__ .
': chunk has invalid CRC, skipping' );
122 if ( $chunk_type ===
"IHDR" ) {
123 $width = unpack(
'N', substr( $buf, 0, 4 ) )[1];
124 $height = unpack(
'N', substr( $buf, 4, 4 ) )[1];
125 $bitDepth = ord( substr( $buf, 8, 1 ) );
128 $colorType = match ( ord( substr( $buf, 9, 1 ) ) ) {
131 3 =>
'index-coloured',
132 4 =>
'greyscale-alpha',
133 6 =>
'truecolour-alpha',
136 } elseif ( $chunk_type ===
"acTL" ) {
137 if ( $chunk_size < 4 ) {
138 wfDebug( __METHOD__ .
": acTL chunk too small" );
142 $actl = unpack(
"Nframes/Nplays", $buf );
143 $frameCount = $actl[
'frames'];
144 $loopCount = $actl[
'plays'];
145 } elseif ( $chunk_type ===
"fcTL" ) {
146 $buf = substr( $buf, 20 );
147 if ( strlen( $buf ) < 4 ) {
148 wfDebug( __METHOD__ .
": fcTL chunk too small" );
152 $fctldur = unpack(
"ndelay_num/ndelay_den", $buf );
153 if ( $fctldur[
'delay_den'] == 0 ) {
154 $fctldur[
'delay_den'] = 100;
156 if ( $fctldur[
'delay_num'] ) {
157 $duration += $fctldur[
'delay_num'] / $fctldur[
'delay_den'];
159 } elseif ( $chunk_type ===
"iTXt" ) {
163 '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds',
172 $items[1] = strtolower( $items[1] );
173 if ( !isset( self::$textChunks[$items[1]] ) ) {
178 $items[3] = strtolower( $items[3] );
179 if ( $items[3] ==
'' ) {
181 $items[3] =
'x-default';
185 if ( $items[2] ===
"\x01" ) {
186 if ( function_exists(
'gzuncompress' ) && $items[4] ===
"\x00" ) {
187 AtEase::suppressWarnings();
188 $items[5] = gzuncompress( $items[5] );
189 AtEase::restoreWarnings();
191 if ( $items[5] ===
false ) {
193 wfDebug( __METHOD__ .
' Error decompressing iTxt chunk - ' . $items[1] );
197 wfDebug( __METHOD__ .
' Skipping compressed png iTXt chunk due to lack of zlib,'
198 .
" or potentially invalid compression method" );
202 $finalKeyword = self::$textChunks[$items[1]];
203 $text[$finalKeyword][$items[3]] = $items[5];
204 $text[$finalKeyword][
'_type'] =
'lang';
207 wfDebug( __METHOD__ .
": Invalid iTXt chunk" );
209 } elseif ( $chunk_type ===
'tEXt' ) {
211 if ( !str_contains( $buf,
"\x00" ) ) {
212 wfDebug( __METHOD__ .
": Invalid tEXt chunk: no null byte" );
216 [ $keyword, $content ] = explode(
"\x00", $buf, 2 );
217 if ( $keyword ===
'' ) {
218 wfDebug( __METHOD__ .
": Empty tEXt keyword" );
223 $keyword = strtolower( $keyword );
224 if ( !isset( self::$textChunks[$keyword] ) ) {
228 AtEase::suppressWarnings();
229 $content = iconv(
'ISO-8859-1',
'UTF-8', $content );
230 AtEase::restoreWarnings();
232 if ( $content ===
false ) {
233 wfDebug( __METHOD__ .
": Read error (error with iconv)" );
237 $finalKeyword = self::$textChunks[$keyword];
238 $text[$finalKeyword][
'x-default'] = $content;
239 $text[$finalKeyword][
'_type'] =
'lang';
240 } elseif ( $chunk_type ===
'zTXt' ) {
241 if ( function_exists(
'gzuncompress' ) ) {
243 if ( !str_contains( $buf,
"\x00" ) ) {
244 wfDebug( __METHOD__ .
": No null byte in zTXt chunk" );
248 [ $keyword, $postKeyword ] = explode(
"\x00", $buf, 2 );
249 if ( $keyword ===
'' || $postKeyword ===
'' ) {
250 wfDebug( __METHOD__ .
": Empty zTXt chunk" );
254 $keyword = strtolower( $keyword );
256 if ( !isset( self::$textChunks[$keyword] ) ) {
260 $compression = substr( $postKeyword, 0, 1 );
261 $content = substr( $postKeyword, 1 );
262 if ( $compression !==
"\x00" ) {
263 wfDebug( __METHOD__ .
" Unrecognized compression method in zTXt ($keyword). Skipping." );
267 AtEase::suppressWarnings();
268 $content = gzuncompress( $content );
269 AtEase::restoreWarnings();
271 if ( $content ===
false ) {
273 wfDebug( __METHOD__ .
' Error decompressing zTXt chunk - ' . $keyword );
277 AtEase::suppressWarnings();
278 $content = iconv(
'ISO-8859-1',
'UTF-8', $content );
279 AtEase::restoreWarnings();
281 if ( $content ===
false ) {
282 wfDebug( __METHOD__ .
": iconv error in zTXt chunk" );
286 $finalKeyword = self::$textChunks[$keyword];
287 $text[$finalKeyword][
'x-default'] = $content;
288 $text[$finalKeyword][
'_type'] =
'lang';
290 wfDebug( __METHOD__ .
" Cannot decompress zTXt chunk due to lack of zlib. Skipping." );
292 } elseif ( $chunk_type ===
'tIME' ) {
294 if ( $chunk_size !== 7 ) {
295 wfDebug( __METHOD__ .
": tIME wrong size" );
300 $t = unpack(
"ny/Cm/Cd/Ch/Cmin/Cs", $buf );
301 $strTime = sprintf(
"%04d%02d%02d%02d%02d%02d",
302 $t[
'y'], $t[
'm'], $t[
'd'], $t[
'h'],
303 $t[
'min'], $t[
's'] );
308 $text[
'DateTime'] = $exifTime;
310 } elseif ( $chunk_type ===
'pHYs' ) {
312 if ( $chunk_size !== 9 ) {
313 wfDebug( __METHOD__ .
": pHYs wrong size" );
317 $dim = unpack(
"Nwidth/Nheight/Cunit", $buf );
318 if ( $dim[
'unit'] === 1 ) {
321 if ( $dim[
'width'] > 0 && $dim[
'height'] > 0 ) {
324 $text[
'XResolution'] = $dim[
'width']
326 $text[
'YResolution'] = $dim[
'height']
328 $text[
'ResolutionUnit'] = 3;
332 } elseif ( $chunk_type ===
"eXIf" ) {
337 substr( $buf, 0, 4 ) !==
"II\x2A\x00" &&
338 substr( $buf, 0, 4 ) !==
"MM\x00\x2A"
341 wfDebug( __METHOD__ .
": Invalid eXIf tag" );
344 } elseif ( $chunk_type ===
"IEND" ) {
350 if ( $loopCount > 1 ) {
351 $duration *= $loopCount;
354 if ( isset( $text[
'DateTimeDigitized'] ) ) {
356 foreach ( $text[
'DateTimeDigitized'] as $name => &$value ) {
357 if ( $name ===
'_type' ) {
384 'frameCount' => $frameCount,
385 'loopCount' => $loopCount,
386 'duration' => $duration,
388 'bitDepth' => $bitDepth,
389 'colorType' => $colorType,