Example #1
0
 function getMetadata($image, $filename)
 {
     if (!isset($image->parsedPNGMetadata)) {
         try {
             $image->parsedPNGMetadata = PNGMetadataExtractor::getMetadata($filename);
         } catch (Exception $e) {
             // Broken file?
             wfDebug(__METHOD__ . ': ' . $e->getMessage() . "\n");
             return '0';
         }
     }
     return serialize($image->parsedPNGMetadata);
 }
 public function testPngGreyscaleNoAlphaColour()
 {
     $meta = PNGMetadataExtractor::getMetadata($this->filePath . 'greyscale-na-png.png');
     $this->assertEquals('greyscale', $meta['colorType']);
 }
 static function getMetadata($filename)
 {
     self::$png_sig = pack("C8", 137, 80, 78, 71, 13, 10, 26, 10);
     self::$CRC_size = 4;
     /* based on list at http://owl.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData
      * and http://www.w3.org/TR/PNG/#11keywords
      */
     self::$text_chunks = array('xml:com.adobe.xmp' => 'xmp', 'artist' => 'Artist', 'model' => 'Model', 'make' => 'Make', 'author' => 'Artist', 'comment' => 'PNGFileComment', 'description' => 'ImageDescription', 'title' => 'ObjectName', 'copyright' => 'Copyright', 'source' => 'Model', 'software' => 'Software', 'disclaimer' => 'Disclaimer', 'warning' => 'ContentWarning', 'url' => 'Identifier', 'label' => 'Label', 'creation time' => 'DateTimeDigitized');
     $frameCount = 0;
     $loopCount = 1;
     $text = array();
     $duration = 0.0;
     $bitDepth = 0;
     $colorType = 'unknown';
     if (!$filename) {
         throw new Exception(__METHOD__ . ": No file name specified");
     } elseif (!file_exists($filename) || is_dir($filename)) {
         throw new Exception(__METHOD__ . ": File {$filename} does not exist");
     }
     $fh = fopen($filename, 'rb');
     if (!$fh) {
         throw new Exception(__METHOD__ . ": Unable to open file {$filename}");
     }
     // Check for the PNG header
     $buf = fread($fh, 8);
     if ($buf != self::$png_sig) {
         throw new Exception(__METHOD__ . ": Not a valid PNG file; header: {$buf}");
     }
     // Read chunks
     while (!feof($fh)) {
         $buf = fread($fh, 4);
         if (!$buf || strlen($buf) < 4) {
             throw new Exception(__METHOD__ . ": Read error");
         }
         $chunk_size = unpack("N", $buf);
         $chunk_size = $chunk_size[1];
         if ($chunk_size < 0) {
             throw new Exception(__METHOD__ . ": Chunk size too big for unpack");
         }
         $chunk_type = fread($fh, 4);
         if (!$chunk_type || strlen($chunk_type) < 4) {
             throw new Exception(__METHOD__ . ": Read error");
         }
         if ($chunk_type == "IHDR") {
             $buf = self::read($fh, $chunk_size);
             if (!$buf || strlen($buf) < $chunk_size) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $bitDepth = ord(substr($buf, 8, 1));
             // Detect the color type in British English as per the spec
             // http://www.w3.org/TR/PNG/#11IHDR
             switch (ord(substr($buf, 9, 1))) {
                 case 0:
                     $colorType = 'greyscale';
                     break;
                 case 2:
                     $colorType = 'truecolour';
                     break;
                 case 3:
                     $colorType = 'index-coloured';
                     break;
                 case 4:
                     $colorType = 'greyscale-alpha';
                     break;
                 case 6:
                     $colorType = 'truecolour-alpha';
                     break;
                 default:
                     $colorType = 'unknown';
                     break;
             }
         } elseif ($chunk_type == "acTL") {
             $buf = fread($fh, $chunk_size);
             if (!$buf || strlen($buf) < $chunk_size || $chunk_size < 4) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $actl = unpack("Nframes/Nplays", $buf);
             $frameCount = $actl['frames'];
             $loopCount = $actl['plays'];
         } elseif ($chunk_type == "fcTL") {
             $buf = self::read($fh, $chunk_size);
             if (!$buf || strlen($buf) < $chunk_size) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $buf = substr($buf, 20);
             if (strlen($buf) < 4) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $fctldur = unpack("ndelay_num/ndelay_den", $buf);
             if ($fctldur['delay_den'] == 0) {
                 $fctldur['delay_den'] = 100;
             }
             if ($fctldur['delay_num']) {
                 $duration += $fctldur['delay_num'] / $fctldur['delay_den'];
             }
         } elseif ($chunk_type == "iTXt") {
             // Extracts iTXt chunks, uncompressing if necessary.
             $buf = self::read($fh, $chunk_size);
             $items = array();
             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::$text_chunks[$items[1]])) {
                     // Only extract textual chunks on our list.
                     fseek($fh, self::$CRC_size, 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] == "") {
                     if (function_exists('gzuncompress') && $items[4] === "") {
                         wfSuppressWarnings();
                         $items[5] = gzuncompress($items[5]);
                         wfRestoreWarnings();
                         if ($items[5] === false) {
                             // decompression failed
                             wfDebug(__METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n");
                             fseek($fh, self::$CRC_size, 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::$CRC_size, SEEK_CUR);
                         continue;
                     }
                 }
                 $finalKeyword = self::$text_chunks[$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, "") === false) {
                 throw new Exception(__METHOD__ . ": Read error on tEXt chunk");
             }
             list($keyword, $content) = explode("", $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::$text_chunks[$keyword])) {
                 // Don't recognize chunk, so skip.
                 fseek($fh, self::$CRC_size, SEEK_CUR);
                 continue;
             }
             wfSuppressWarnings();
             $content = iconv('ISO-8859-1', 'UTF-8', $content);
             wfRestoreWarnings();
             if ($content === false) {
                 throw new Exception(__METHOD__ . ": Read error (error with iconv)");
             }
             $finalKeyword = self::$text_chunks[$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, "") === false) {
                     throw new Exception(__METHOD__ . ": Read error on zTXt chunk");
                 }
                 list($keyword, $postKeyword) = explode("", $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::$text_chunks[$keyword])) {
                     // Don't recognize chunk, so skip.
                     fseek($fh, self::$CRC_size, SEEK_CUR);
                     continue;
                 }
                 $compression = substr($postKeyword, 0, 1);
                 $content = substr($postKeyword, 1);
                 if ($compression !== "") {
                     wfDebug(__METHOD__ . " Unrecognized compression method in zTXt ({$keyword}). Skipping.\n");
                     fseek($fh, self::$CRC_size, SEEK_CUR);
                     continue;
                 }
                 wfSuppressWarnings();
                 $content = gzuncompress($content);
                 wfRestoreWarnings();
                 if ($content === false) {
                     // decompression failed
                     wfDebug(__METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n");
                     fseek($fh, self::$CRC_size, SEEK_CUR);
                     continue;
                 }
                 wfSuppressWarnings();
                 $content = iconv('ISO-8859-1', 'UTF-8', $content);
                 wfRestoreWarnings();
                 if ($content === false) {
                     throw new Exception(__METHOD__ . ": Read error (error with iconv)");
                 }
                 $finalKeyword = self::$text_chunks[$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).
                 }
             }
         } elseif ($chunk_type == "IEND") {
             break;
         } else {
             fseek($fh, $chunk_size, SEEK_CUR);
         }
         fseek($fh, self::$CRC_size, SEEK_CUR);
     }
     fclose($fh);
     if ($loopCount > 1) {
         $duration *= $loopCount;
     }
     if (isset($text['DateTimeDigitized'])) {
         // Convert date format from rfc2822 to exif.
         foreach ($text['DateTimeDigitized'] as $name => &$value) {
             if ($name === '_type') {
                 continue;
             }
             // @todo FIXME: Currently timezones are ignored.
             // possibly should be wfTimestamp's
             // responsibility. (at least for numeric TZ)
             $formatted = wfTimestamp(TS_EXIF, $value);
             if ($formatted) {
                 // Only change if we could convert the
                 // date.
                 // The png standard says it should be
                 // in rfc2822 format, but not required.
                 // In general for the exif stuff we
                 // prettify the date if we can, but we
                 // display as-is if we cannot or if
                 // it is invalid.
                 // So do the same here.
                 $value = $formatted;
             }
         }
     }
     return array('frameCount' => $frameCount, 'loopCount' => $loopCount, 'duration' => $duration, 'text' => $text, 'bitDepth' => $bitDepth, 'colorType' => $colorType);
 }
 /** Entry point for png
  * At some point in the future this might
  * merge the png various tEXt chunks to that
  * are interesting, but for now it only does XMP
  *
  * @param string $filename full path to file
  * @return Array Array for storage in img_metadata.
  */
 public static function PNG($filename)
 {
     $showXMP = function_exists('xml_parser_create_ns');
     $meta = new self();
     $array = PNGMetadataExtractor::getMetadata($filename);
     if (isset($array['text']['xmp']['x-default']) && $array['text']['xmp']['x-default'] !== '' && $showXMP) {
         $xmp = new XMPReader();
         $xmp->parse($array['text']['xmp']['x-default']);
         $xmpRes = $xmp->getResults();
         foreach ($xmpRes as $type => $xmpSection) {
             $meta->addMetadata($xmpSection, $type);
         }
     }
     unset($array['text']['xmp']);
     $meta->addMetadata($array['text'], 'native');
     unset($array['text']);
     $array['metadata'] = $meta->getMetadataArray();
     $array['metadata']['_MW_PNG_VERSION'] = PNGMetadataExtractor::VERSION;
     return $array;
 }
 /** Entry point for png
  * At some point in the future this might
  * merge the png various tEXt chunks to that
  * are interesting, but for now it only does XMP
  *
  * @param string $filename Full path to file
  * @return array Array for storage in img_metadata.
  */
 public static function PNG($filename)
 {
     $showXMP = XMPReader::isSupported();
     $meta = new self();
     $array = PNGMetadataExtractor::getMetadata($filename);
     if (isset($array['text']['xmp']['x-default']) && $array['text']['xmp']['x-default'] !== '' && $showXMP) {
         $xmp = new XMPReader(LoggerFactory::getInstance('XMP'));
         $xmp->parse($array['text']['xmp']['x-default']);
         $xmpRes = $xmp->getResults();
         foreach ($xmpRes as $type => $xmpSection) {
             $meta->addMetadata($xmpSection, $type);
         }
     }
     unset($array['text']['xmp']);
     $meta->addMetadata($array['text'], 'native');
     unset($array['text']);
     $array['metadata'] = $meta->getMetadataArray();
     $array['metadata']['_MW_PNG_VERSION'] = PNGMetadataExtractor::VERSION;
     return $array;
 }
 static function getMetadata($filename)
 {
     self::$png_sig = pack("C8", 137, 80, 78, 71, 13, 10, 26, 10);
     self::$CRC_size = 4;
     $frameCount = 0;
     $loopCount = 1;
     $duration = 0.0;
     if (!$filename) {
         throw new Exception(__METHOD__ . ": No file name specified");
     } elseif (!file_exists($filename) || is_dir($filename)) {
         throw new Exception(__METHOD__ . ": File {$filename} does not exist");
     }
     $fh = fopen($filename, 'r');
     if (!$fh) {
         throw new Exception(__METHOD__ . ": Unable to open file {$filename}");
     }
     // Check for the PNG header
     $buf = fread($fh, 8);
     if ($buf != self::$png_sig) {
         throw new Exception(__METHOD__ . ": Not a valid PNG file; header: {$buf}");
     }
     // Read chunks
     while (!feof($fh)) {
         $buf = fread($fh, 4);
         if (!$buf) {
             throw new Exception(__METHOD__ . ": Read error");
         }
         $chunk_size = unpack("N", $buf);
         $chunk_size = $chunk_size[1];
         $chunk_type = fread($fh, 4);
         if (!$chunk_type) {
             throw new Exception(__METHOD__ . ": Read error");
         }
         if ($chunk_type == "acTL") {
             $buf = fread($fh, $chunk_size);
             if (!$buf) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $actl = unpack("Nframes/Nplays", $buf);
             $frameCount = $actl['frames'];
             $loopCount = $actl['plays'];
         } elseif ($chunk_type == "fcTL") {
             $buf = fread($fh, $chunk_size);
             if (!$buf) {
                 throw new Exception(__METHOD__ . ": Read error");
             }
             $buf = substr($buf, 20);
             $fctldur = unpack("ndelay_num/ndelay_den", $buf);
             if ($fctldur['delay_den'] == 0) {
                 $fctldur['delay_den'] = 100;
             }
             if ($fctldur['delay_num']) {
                 $duration += $fctldur['delay_num'] / $fctldur['delay_den'];
             }
         } elseif (($chunk_type == "IDAT" || $chunk_type == "IEND") && $frameCount == 0) {
             // Not a valid animated image. No point in continuing.
             break;
         } elseif ($chunk_type == "IEND") {
             break;
         } else {
             fseek($fh, $chunk_size, SEEK_CUR);
         }
         fseek($fh, self::$CRC_size, SEEK_CUR);
     }
     fclose($fh);
     if ($loopCount > 1) {
         $duration *= $loopCount;
     }
     return array('frameCount' => $frameCount, 'loopCount' => $loopCount, 'duration' => $duration);
 }