+ } elseif ( $chunk_type == "iTXt" ) {
+ // Extracts iTXt chunks, uncompressing if necessary.
+ $buf = self::read( $fh, $chunk_size );
+ $items = [];
+ if ( preg_match(
+ '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds',
+ $buf, $items )
+ ) {
+ /* $items[1] = text chunk name, $items[2] = compressed flag,
+ * $items[3] = lang code (or ""), $items[4]= compression type.
+ * $items[5] = content
+ */
+
+ // Theoretically should be case-sensitive, but in practise...
+ $items[1] = strtolower( $items[1] );
+ if ( !isset( self::$textChunks[$items[1]] ) ) {
+ // Only extract textual chunks on our list.
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+
+ $items[3] = strtolower( $items[3] );
+ if ( $items[3] == '' ) {
+ // if no lang specified use x-default like in xmp.
+ $items[3] = 'x-default';
+ }
+
+ // if compressed
+ if ( $items[2] == "\x01" ) {
+ if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) {
+ MediaWiki\suppressWarnings();
+ $items[5] = gzuncompress( $items[5] );
+ MediaWiki\restoreWarnings();
+
+ if ( $items[5] === false ) {
+ // decompression failed
+ wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" );
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+ } else {
+ wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,'
+ . " or potentially invalid compression method\n" );
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+ }
+ $finalKeyword = self::$textChunks[$items[1]];
+ $text[$finalKeyword][$items[3]] = $items[5];
+ $text[$finalKeyword]['_type'] = 'lang';
+ } else {
+ // Error reading iTXt chunk
+ throw new Exception( __METHOD__ . ": Read error on iTXt chunk" );
+ }
+ } elseif ( $chunk_type == 'tEXt' ) {
+ $buf = self::read( $fh, $chunk_size );
+
+ // In case there is no \x00 which will make explode fail.
+ if ( strpos( $buf, "\x00" ) === false ) {
+ throw new Exception( __METHOD__ . ": Read error on tEXt chunk" );
+ }
+
+ list( $keyword, $content ) = explode( "\x00", $buf, 2 );
+ if ( $keyword === '' || $content === '' ) {
+ throw new Exception( __METHOD__ . ": Read error on tEXt chunk" );
+ }
+
+ // Theoretically should be case-sensitive, but in practise...
+ $keyword = strtolower( $keyword );
+ if ( !isset( self::$textChunks[$keyword] ) ) {
+ // Don't recognize chunk, so skip.
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+ MediaWiki\suppressWarnings();
+ $content = iconv( 'ISO-8859-1', 'UTF-8', $content );
+ MediaWiki\restoreWarnings();
+
+ if ( $content === false ) {
+ throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
+ }
+
+ $finalKeyword = self::$textChunks[$keyword];
+ $text[$finalKeyword]['x-default'] = $content;
+ $text[$finalKeyword]['_type'] = 'lang';
+ } elseif ( $chunk_type == 'zTXt' ) {
+ if ( function_exists( 'gzuncompress' ) ) {
+ $buf = self::read( $fh, $chunk_size );
+
+ // In case there is no \x00 which will make explode fail.
+ if ( strpos( $buf, "\x00" ) === false ) {
+ throw new Exception( __METHOD__ . ": Read error on zTXt chunk" );
+ }
+
+ list( $keyword, $postKeyword ) = explode( "\x00", $buf, 2 );
+ if ( $keyword === '' || $postKeyword === '' ) {
+ throw new Exception( __METHOD__ . ": Read error on zTXt chunk" );
+ }
+ // Theoretically should be case-sensitive, but in practise...
+ $keyword = strtolower( $keyword );
+
+ if ( !isset( self::$textChunks[$keyword] ) ) {
+ // Don't recognize chunk, so skip.
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+ $compression = substr( $postKeyword, 0, 1 );
+ $content = substr( $postKeyword, 1 );
+ if ( $compression !== "\x00" ) {
+ wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" );
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+
+ MediaWiki\suppressWarnings();
+ $content = gzuncompress( $content );
+ MediaWiki\restoreWarnings();
+
+ if ( $content === false ) {
+ // decompression failed
+ wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" );
+ fseek( $fh, self::$crcSize, SEEK_CUR );
+ continue;
+ }
+
+ MediaWiki\suppressWarnings();
+ $content = iconv( 'ISO-8859-1', 'UTF-8', $content );
+ MediaWiki\restoreWarnings();
+
+ if ( $content === false ) {
+ throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
+ }
+
+ $finalKeyword = self::$textChunks[$keyword];
+ $text[$finalKeyword]['x-default'] = $content;
+ $text[$finalKeyword]['_type'] = 'lang';
+ } else {
+ wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" );
+ fseek( $fh, $chunk_size, SEEK_CUR );
+ }
+ } elseif ( $chunk_type == 'tIME' ) {
+ // last mod timestamp.
+ if ( $chunk_size !== 7 ) {
+ throw new Exception( __METHOD__ . ": tIME wrong size" );
+ }
+ $buf = self::read( $fh, $chunk_size );
+ if ( !$buf || strlen( $buf ) < $chunk_size ) {
+ throw new Exception( __METHOD__ . ": Read error" );
+ }
+
+ // Note: spec says this should be UTC.
+ $t = unpack( "ny/Cm/Cd/Ch/Cmin/Cs", $buf );
+ $strTime = sprintf( "%04d%02d%02d%02d%02d%02d",
+ $t['y'], $t['m'], $t['d'], $t['h'],
+ $t['min'], $t['s'] );
+
+ $exifTime = wfTimestamp( TS_EXIF, $strTime );
+
+ if ( $exifTime ) {
+ $text['DateTime'] = $exifTime;
+ }
+ } elseif ( $chunk_type == 'pHYs' ) {
+ // how big pixels are (dots per meter).
+ if ( $chunk_size !== 9 ) {
+ throw new Exception( __METHOD__ . ": pHYs wrong size" );
+ }
+
+ $buf = self::read( $fh, $chunk_size );
+ if ( !$buf || strlen( $buf ) < $chunk_size ) {
+ throw new Exception( __METHOD__ . ": Read error" );
+ }
+
+ $dim = unpack( "Nwidth/Nheight/Cunit", $buf );
+ if ( $dim['unit'] == 1 ) {
+ // Need to check for negative because php
+ // doesn't deal with super-large unsigned 32-bit ints well
+ if ( $dim['width'] > 0 && $dim['height'] > 0 ) {
+ // unit is meters
+ // (as opposed to 0 = undefined )
+ $text['XResolution'] = $dim['width']
+ . '/100';
+ $text['YResolution'] = $dim['height']
+ . '/100';
+ $text['ResolutionUnit'] = 3;
+ // 3 = dots per cm (from Exif).
+ }
+ }