diff --git a/lib/getid3/extension.cache.dbm.php b/lib/getid3/extension.cache.dbm.php index a9332b9..bf94ef1 100644 --- a/lib/getid3/extension.cache.dbm.php +++ b/lib/getid3/extension.cache.dbm.php @@ -1,10 +1,10 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // // // // extension.cache.dbm.php - part of getID3() // // Please see readme.txt for more information // @@ -72,8 +72,36 @@ class getID3_cached_dbm extends getID3 { + /** + * @var resource + */ + private $dba; - // public: constructor - see top of this file for cache type and cache_options + /** + * @var resource|bool + */ + private $lock; + + /** + * @var string + */ + private $cache_type; + + /** + * @var string + */ + private $dbm_filename; + + /** + * constructor - see top of this file for cache type and cache_options + * + * @param string $cache_type + * @param string $dbm_filename + * @param string $lock_filename + * + * @throws Exception + * @throws getid3_exception + */ public function __construct($cache_type, $dbm_filename, $lock_filename) { // Check for dba extension @@ -141,7 +169,9 @@ class getID3_cached_dbm extends getID3 - // public: destructor + /** + * destructor + */ public function __destruct() { // Close dbm file @@ -156,7 +186,11 @@ class getID3_cached_dbm extends getID3 - // public: clear cache + /** + * clear cache + * + * @throws Exception + */ public function clear_cache() { // Close dbm file @@ -178,9 +212,19 @@ class getID3_cached_dbm extends getID3 - // public: analyze file - public function analyze($filename) { + /** + * clear cache + * + * @param string $filename + * @param int $filesize + * @param string $original_filename + * @param resource $fp + * + * @return mixed + */ + public function analyze($filename, $filesize=null, $original_filename='', $fp=null) { + $key = null; if (file_exists($filename)) { // Calc key filename::mod_time::size - should be unique @@ -196,10 +240,10 @@ class getID3_cached_dbm extends getID3 } // Miss - $result = parent::analyze($filename); + $result = parent::analyze($filename, $filesize, $original_filename, $fp); // Save result - if (file_exists($filename)) { + if (isset($key) && file_exists($filename)) { dba_insert($key, serialize($result), $this->dba); } diff --git a/lib/getid3/extension.cache.mysql.php b/lib/getid3/extension.cache.mysql.php index 305064d..6444bcd 100644 --- a/lib/getid3/extension.cache.mysql.php +++ b/lib/getid3/extension.cache.mysql.php @@ -1,10 +1,10 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // // // // extension.cache.mysql.php - part of getID3() // // Please see readme.txt for more information // @@ -73,13 +73,34 @@ class getID3_cached_mysql extends getID3 { - - // private vars + /** + * @var resource + */ private $cursor; + + /** + * @var resource + */ private $connection; + /** + * @var string + */ + private $table; - // public: constructor - see top of this file for cache type and cache_options + + /** + * constructor - see top of this file for cache type and cache_options + * + * @param string $host + * @param string $database + * @param string $username + * @param string $password + * @param string $table + * + * @throws Exception + * @throws getid3_exception + */ public function __construct($host, $database, $username, $password, $table='getid3_cache') { // Check for mysql support @@ -124,7 +145,9 @@ class getID3_cached_mysql extends getID3 - // public: clear cache + /** + * clear cache + */ public function clear_cache() { $this->cursor = mysql_query('DELETE FROM `'.mysql_real_escape_string($this->table).'`', $this->connection); @@ -133,9 +156,19 @@ class getID3_cached_mysql extends getID3 - // public: analyze file - public function analyze($filename, $filesize=null, $original_filename='') { + /** + * analyze file + * + * @param string $filename + * @param int $filesize + * @param string $original_filename + * @param resource $fp + * + * @return mixed + */ + public function analyze($filename, $filesize=null, $original_filename='', $fp=null) { + $filetime = 0; if (file_exists($filename)) { // Short-hands @@ -157,7 +190,7 @@ class getID3_cached_mysql extends getID3 } // Miss - $analysis = parent::analyze($filename, $filesize, $original_filename); + $analysis = parent::analyze($filename, $filesize, $original_filename, $fp); // Save result if (file_exists($filename)) { @@ -174,7 +207,11 @@ class getID3_cached_mysql extends getID3 - // private: (re)create sql table + /** + * (re)create sql table + * + * @param bool $drop + */ private function create_table($drop=false) { $SQLquery = 'CREATE TABLE IF NOT EXISTS `'.mysql_real_escape_string($this->table).'` ('; diff --git a/lib/getid3/extension.cache.mysqli.php b/lib/getid3/extension.cache.mysqli.php index 185e831..cee2c53 100644 --- a/lib/getid3/extension.cache.mysqli.php +++ b/lib/getid3/extension.cache.mysqli.php @@ -1,14 +1,14 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // // // -// extension.cache.mysqli.php - part of getID3() // +// extension.cache.mysqli.php - part of getID3() // // Please see readme.txt for more information // -// /// +// // ///////////////////////////////////////////////////////////////// // // // This extension written by Allan Hansen // @@ -72,12 +72,39 @@ class getID3_cached_mysqli extends getID3 { - // private vars + /** + * @var mysqli + */ private $mysqli; + + /** + * @var mysqli_result + */ private $cursor; + /** + * @var string + */ + private $table; - // public: constructor - see top of this file for cache type and cache_options + /** + * @var bool + */ + private $db_structure_check; + + + /** + * constructor - see top of this file for cache type and cache_options + * + * @param string $host + * @param string $database + * @param string $username + * @param string $password + * @param string $table + * + * @throws Exception + * @throws getid3_exception + */ public function __construct($host, $database, $username, $password, $table='getid3_cache') { // Check for mysqli support @@ -87,8 +114,8 @@ class getID3_cached_mysqli extends getID3 // Connect to database $this->mysqli = new mysqli($host, $username, $password); - if (!$this->mysqli) { - throw new Exception('mysqli_connect() failed - check permissions and spelling.'); + if ($this->mysqli->connect_error) { + throw new Exception('Connect Error (' . $this->mysqli->connect_errno . ') ' . $this->mysqli->connect_error); } // Select database @@ -102,14 +129,15 @@ class getID3_cached_mysqli extends getID3 // Create cache table if not exists $this->create_table(); + $this->db_structure_check = true; // set to false if you know your table structure has already been migrated to use `hash` as the primary key to avoid + $this->migrate_db_structure(); + // Check version number and clear cache if changed $version = ''; $SQLquery = 'SELECT `value`'; $SQLquery .= ' FROM `'.$this->mysqli->real_escape_string($this->table).'`'; $SQLquery .= ' WHERE (`filename` = \''.$this->mysqli->real_escape_string(getID3::VERSION).'\')'; - $SQLquery .= ' AND (`filesize` = -1)'; - $SQLquery .= ' AND (`filetime` = -1)'; - $SQLquery .= ' AND (`analyzetime` = -1)'; + $SQLquery .= ' AND (`hash` = \'getID3::VERSION\')'; if ($this->cursor = $this->mysqli->query($SQLquery)) { list($version) = $this->cursor->fetch_array(); } @@ -121,16 +149,59 @@ class getID3_cached_mysqli extends getID3 } - // public: clear cache + /** + * clear cache + */ public function clear_cache() { - $this->mysqli->query('DELETE FROM `'.$this->mysqli->real_escape_string($this->table).'`'); - $this->mysqli->query('INSERT INTO `'.$this->mysqli->real_escape_string($this->table).'` (`filename`, `filesize`, `filetime`, `analyzetime`, `value`) VALUES (\''.getID3::VERSION.'\', -1, -1, -1, \''.getID3::VERSION.'\')'); + $this->mysqli->query('TRUNCATE TABLE `'.$this->mysqli->real_escape_string($this->table).'`'); + $this->mysqli->query('INSERT INTO `'.$this->mysqli->real_escape_string($this->table).'` (`hash`, `filename`, `filesize`, `filetime`, `analyzetime`, `value`) VALUES (\'getID3::VERSION\', \''.getID3::VERSION.'\', -1, -1, -1, \''.getID3::VERSION.'\')'); } - // public: analyze file - public function analyze($filename, $filesize=null, $original_filename='') { + /** + * migrate database structure if needed + */ + public function migrate_db_structure() { + // Check for table structure + if ($this->db_structure_check) { + $SQLquery = 'SHOW COLUMNS'; + $SQLquery .= ' FROM `'.$this->mysqli->real_escape_string($this->table).'`'; + $SQLquery .= ' LIKE \'hash\''; + $this->cursor = $this->mysqli->query($SQLquery); + if ($this->cursor->num_rows == 0) { + // table has not been migrated, add column, add hashes, change index + $SQLquery = 'ALTER TABLE `getid3_cache` DROP PRIMARY KEY, ADD `hash` CHAR(32) NOT NULL DEFAULT \'\' FIRST, ADD PRIMARY KEY(`hash`)'; + $this->mysqli->query($SQLquery); + $SQLquery = 'UPDATE `getid3_cache` SET'; + $SQLquery .= ' `hash` = MD5(`filename`, `filesize`, `filetime`)'; + $SQLquery .= ' WHERE (`filesize` > -1)'; + $this->mysqli->query($SQLquery); + + $SQLquery = 'UPDATE `getid3_cache` SET'; + $SQLquery .= ' `hash` = \'getID3::VERSION\''; + $SQLquery .= ' WHERE (`filesize` = -1)'; + $SQLquery .= ' AND (`filetime` = -1)'; + $SQLquery .= ' AND (`filetime` = -1)'; + $this->mysqli->query($SQLquery); + } + } + } + + + /** + * analyze file + * + * @param string $filename + * @param int $filesize + * @param string $original_filename + * @param resource $fp + * + * @return mixed + */ + public function analyze($filename, $filesize=null, $original_filename='', $fp=null) { + + $filetime = 0; if (file_exists($filename)) { // Short-hands @@ -140,9 +211,7 @@ class getID3_cached_mysqli extends getID3 // Lookup file $SQLquery = 'SELECT `value`'; $SQLquery .= ' FROM `'.$this->mysqli->real_escape_string($this->table).'`'; - $SQLquery .= ' WHERE (`filename` = \''.$this->mysqli->real_escape_string($filename).'\')'; - $SQLquery .= ' AND (`filesize` = \''.$this->mysqli->real_escape_string($filesize).'\')'; - $SQLquery .= ' AND (`filetime` = \''.$this->mysqli->real_escape_string($filetime).'\')'; + $SQLquery .= ' WHERE (`hash` = \''.$this->mysqli->real_escape_string(md5($filename.$filesize.$filetime)).'\')'; $this->cursor = $this->mysqli->query($SQLquery); if ($this->cursor->num_rows > 0) { // Hit @@ -152,32 +221,43 @@ class getID3_cached_mysqli extends getID3 } // Miss - $analysis = parent::analyze($filename, $filesize, $original_filename); + $analysis = parent::analyze($filename, $filesize, $original_filename, $fp); // Save result if (file_exists($filename)) { - $SQLquery = 'INSERT INTO `'.$this->mysqli->real_escape_string($this->table).'` (`filename`, `filesize`, `filetime`, `analyzetime`, `value`) VALUES ('; - $SQLquery .= '\''.$this->mysqli->real_escape_string($filename).'\''; + $SQLquery = 'INSERT INTO `'.$this->mysqli->real_escape_string($this->table).'` (`hash`, `filename`, `filesize`, `filetime`, `analyzetime`, `value`) VALUES ('; + $SQLquery .= '\''.$this->mysqli->real_escape_string(md5($filename.$filesize.$filetime)).'\''; + $SQLquery .= ', \''.$this->mysqli->real_escape_string($filename).'\''; $SQLquery .= ', \''.$this->mysqli->real_escape_string($filesize).'\''; $SQLquery .= ', \''.$this->mysqli->real_escape_string($filetime).'\''; - $SQLquery .= ', \''.$this->mysqli->real_escape_string(time() ).'\''; - $SQLquery .= ', \''.$this->mysqli->real_escape_string(base64_encode(serialize($analysis))).'\')'; + $SQLquery .= ', UNIX_TIMESTAMP()'; + $SQLquery .= ', \''.$this->mysqli->real_escape_string(base64_encode(serialize($analysis))).'\''; + $SQLquery .= ')'; $this->cursor = $this->mysqli->query($SQLquery); } return $analysis; } - // private: (re)create mysqli table + /** + * (re)create mysqli table + * + * @param bool $drop + */ private function create_table($drop=false) { + if ($drop) { + $SQLquery = 'DROP TABLE IF EXISTS `'.$this->mysqli->real_escape_string($this->table).'`'; + $this->mysqli->query($SQLquery); + } $SQLquery = 'CREATE TABLE IF NOT EXISTS `'.$this->mysqli->real_escape_string($this->table).'` ('; - $SQLquery .= '`filename` VARCHAR(990) NOT NULL DEFAULT \'\''; + $SQLquery .= '`hash` CHAR(32) NOT NULL DEFAULT \'\''; + $SQLquery .= ', `filename` VARCHAR(1000) NOT NULL DEFAULT \'\''; $SQLquery .= ', `filesize` INT(11) NOT NULL DEFAULT \'0\''; $SQLquery .= ', `filetime` INT(11) NOT NULL DEFAULT \'0\''; $SQLquery .= ', `analyzetime` INT(11) NOT NULL DEFAULT \'0\''; $SQLquery .= ', `value` LONGTEXT NOT NULL'; - $SQLquery .= ', PRIMARY KEY (`filename`, `filesize`, `filetime`))'; + $SQLquery .= ', PRIMARY KEY (`hash`))'; $this->cursor = $this->mysqli->query($SQLquery); echo $this->mysqli->error; } -} \ No newline at end of file +} diff --git a/lib/getid3/extension.cache.sqlite3.php b/lib/getid3/extension.cache.sqlite3.php index dccd78d..dbcc72a 100644 --- a/lib/getid3/extension.cache.sqlite3.php +++ b/lib/getid3/extension.cache.sqlite3.php @@ -1,21 +1,27 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////////////////////// -/// // -// extension.cache.sqlite3.php - part of getID3() // -// Please see readme.txt for more information // -// /// -///////////////////////////////////////////////////////////////////////////////// -/// // -// MySQL extension written by Allan Hansen // -// Table name mod by Carlo Capocasa // -// MySQL extension was reworked for SQLite3 by Karl G. Holz // -// /// -///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////// +/// getID3() by James Heinrich // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// // +// extension.cache.mysqli.php - part of getID3() // +// Please see readme.txt for more information // +// // +///////////////////////////////////////////////////////////////// +// // +// extension.cache.sqlite3.php - part of getID3() // +// Please see readme.txt for more information // +// // +///////////////////////////////////////////////////////////////// +/// // +// MySQL extension written by Allan Hansen // +// Table name mod by Carlo Capocasa // +// MySQL extension was reworked for SQLite3 by // +// Karl G. Holz // +// /// +///////////////////////////////////////////////////////////////// + /** * This is a caching extension for getID3(). It works the exact same * way as the getID3 class, but return cached information much faster @@ -89,14 +95,35 @@ * * */ -class getID3_cached_sqlite3 extends getID3 { +class getID3_cached_sqlite3 extends getID3 +{ + /** + * hold the sqlite db + * + * @var SQLite3 Resource + */ + private $db; /** - * __construct() - * @param string $table holds name of sqlite table - * @return type - */ + * table to use for caching + * + * @var string $table + */ + private $table; + + /** + * @param string $table holds name of sqlite table + * @param boolean $hide + * + * @throws getid3_exception + * @throws Exception + */ public function __construct($table='getid3_cache', $hide=false) { + // Check for SQLite3 support + if (!function_exists('sqlite_open')) { + throw new Exception('PHP not compiled with SQLite3 support.'); + } + $this->table = $table; // Set table $file = dirname(__FILE__).'/'.basename(__FILE__, 'php').'sqlite'; if ($hide) { @@ -106,7 +133,7 @@ class getID3_cached_sqlite3 extends getID3 { $db = $this->db; $this->create_table(); // Create cache table if not exists $version = ''; - $sql = $this->version_check; + $sql = $this->getQuery('version_check'); $stmt = $db->prepare($sql); $stmt->bindValue(':filename', getID3::VERSION, SQLITE3_TEXT); $result = $stmt->execute(); @@ -114,39 +141,27 @@ class getID3_cached_sqlite3 extends getID3 { if ($version != getID3::VERSION) { // Check version number and clear cache if changed $this->clear_cache(); } - return parent::__construct(); + parent::__construct(); } /** - * close the database connection - */ + * close the database connection + */ public function __destruct() { $db=$this->db; $db->close(); } /** - * hold the sqlite db - * @var SQLite Resource - */ - private $db; - - /** - * table to use for caching - * @var string $table - */ - private $table; - - /** - * clear the cache - * @access private - * @return type - */ + * clear the cache + * + * @return SQLite3Result + */ private function clear_cache() { $db = $this->db; - $sql = $this->delete_cache; + $sql = $this->getQuery('delete_cache'); $db->exec($sql); - $sql = $this->set_version; + $sql = $this->getQuery('set_version'); $stmt = $db->prepare($sql); $stmt->bindValue(':filename', getID3::VERSION, SQLITE3_TEXT); $stmt->bindValue(':dirname', getID3::VERSION, SQLITE3_TEXT); @@ -155,11 +170,16 @@ class getID3_cached_sqlite3 extends getID3 { } /** - * analyze file and cache them, if cached pull from the db - * @param type $filename - * @return boolean - */ - public function analyze($filename, $filesize=null, $original_filename='') { + * analyze file and cache them, if cached pull from the db + * + * @param string $filename + * @param integer $filesize + * @param string $original_filename + * @param resource $fp + * + * @return mixed|false + */ + public function analyze($filename, $filesize=null, $original_filename='', $fp=null) { if (!file_exists($filename)) { return false; } @@ -171,7 +191,7 @@ class getID3_cached_sqlite3 extends getID3 { $dirname = dirname($filename); // Lookup file $db = $this->db; - $sql = $this->get_id3_data; + $sql = $this->getQuery('get_id3_data'); $stmt = $db->prepare($sql); $stmt->bindValue(':filename', $filename, SQLITE3_TEXT); $stmt->bindValue(':filesize', $filesize_real, SQLITE3_INTEGER); @@ -182,9 +202,9 @@ class getID3_cached_sqlite3 extends getID3 { return unserialize(base64_decode($result)); } // if it hasn't been analyzed before, then do it now - $analysis = parent::analyze($filename, $filesize, $original_filename); + $analysis = parent::analyze($filename, $filesize, $original_filename, $fp); // Save result - $sql = $this->cache_file; + $sql = $this->getQuery('cache_file'); $stmt = $db->prepare($sql); $stmt->bindValue(':filename', $filename, SQLITE3_TEXT); $stmt->bindValue(':dirname', $dirname, SQLITE3_TEXT); @@ -197,30 +217,31 @@ class getID3_cached_sqlite3 extends getID3 { } /** - * create data base table - * this is almost the same as MySQL, with the exception of the dirname being added - * @return type - */ + * create data base table + * this is almost the same as MySQL, with the exception of the dirname being added + * + * @return bool + */ private function create_table() { $db = $this->db; - $sql = $this->make_table; + $sql = $this->getQuery('make_table'); return $db->exec($sql); } /** - * get cached directory - * - * This function is not in the MySQL extention, it's ment to speed up requesting multiple files - * which is ideal for podcasting, playlists, etc. - * - * @access public - * @param string $dir directory to search the cache database for - * @return array return an array of matching id3 data - */ + * get cached directory + * + * This function is not in the MySQL extention, it's ment to speed up requesting multiple files + * which is ideal for podcasting, playlists, etc. + * + * @param string $dir directory to search the cache database for + * + * @return array return an array of matching id3 data + */ public function get_cached_dir($dir) { $db = $this->db; $rows = array(); - $sql = $this->get_cached_dir; + $sql = $this->getQuery('get_cached_dir'); $stmt = $db->prepare($sql); $stmt->bindValue(':dirname', $dir, SQLITE3_TEXT); $res = $stmt->execute(); @@ -230,36 +251,47 @@ class getID3_cached_sqlite3 extends getID3 { return $rows; } + /** + * returns NULL if query is not found + * + * @param string $name + * + * @return null|string + */ + public function getQuery($name) + { + switch ($name) { + case 'version_check': + return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = '-1' AND filetime = '-1' AND analyzetime = '-1'"; + case 'delete_cache': + return "DELETE FROM $this->table"; + case 'set_version': + return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, -1, -1, -1, :val)"; + case 'get_id3_data': + return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = :filesize AND filetime = :filetime"; + case 'cache_file': + return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, :filesize, :filetime, :atime, :val)"; + case 'make_table': + return "CREATE TABLE IF NOT EXISTS $this->table (filename VARCHAR(255) DEFAULT '', dirname VARCHAR(255) DEFAULT '', filesize INT(11) DEFAULT '0', filetime INT(11) DEFAULT '0', analyzetime INT(11) DEFAULT '0', val text, PRIMARY KEY (filename, filesize, filetime))"; + case 'get_cached_dir': + return "SELECT val FROM $this->table WHERE dirname = :dirname"; + default: + return null; + } + } + /** * use the magical __get() for sql queries * * access as easy as $this->{case name}, returns NULL if query is not found + * + * @param string $name + * + * @return string + * @deprecated use getQuery() instead */ public function __get($name) { - switch($name) { - case 'version_check': - return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = '-1' AND filetime = '-1' AND analyzetime = '-1'"; - break; - case 'delete_cache': - return "DELETE FROM $this->table"; - break; - case 'set_version': - return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, -1, -1, -1, :val)"; - break; - case 'get_id3_data': - return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = :filesize AND filetime = :filetime"; - break; - case 'cache_file': - return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, :filesize, :filetime, :atime, :val)"; - break; - case 'make_table': - return "CREATE TABLE IF NOT EXISTS $this->table (filename VARCHAR(255) DEFAULT '', dirname VARCHAR(255) DEFAULT '', filesize INT(11) DEFAULT '0', filetime INT(11) DEFAULT '0', analyzetime INT(11) DEFAULT '0', val text, PRIMARY KEY (filename, filesize, filetime))"; - break; - case 'get_cached_dir': - return "SELECT val FROM $this->table WHERE dirname = :dirname"; - break; - } - return null; + return $this->getQuery($name); } } diff --git a/lib/getid3/getid3.lib.php b/lib/getid3/getid3.lib.php index 1931bb3..aec550b 100644 --- a/lib/getid3/getid3.lib.php +++ b/lib/getid3/getid3.lib.php @@ -1,27 +1,41 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // // // // getid3.lib.php - part of getID3() // -// See readme.txt for more details // +// see readme.txt for more details // // /// ///////////////////////////////////////////////////////////////// +if(!defined('GETID3_LIBXML_OPTIONS') && defined('LIBXML_VERSION')) { + if(LIBXML_VERSION >= 20621) { + define('GETID3_LIBXML_OPTIONS', LIBXML_NOENT | LIBXML_NONET | LIBXML_NOWARNING | LIBXML_COMPACT); + } else { + define('GETID3_LIBXML_OPTIONS', LIBXML_NOENT | LIBXML_NONET | LIBXML_NOWARNING); + } +} class getid3_lib { - + /** + * @param string $string + * @param bool $hex + * @param bool $spaces + * @param string|bool $htmlencoding + * + * @return string + */ public static function PrintHexBytes($string, $hex=true, $spaces=true, $htmlencoding='UTF-8') { $returnstring = ''; for ($i = 0; $i < strlen($string); $i++) { if ($hex) { - $returnstring .= str_pad(dechex(ord($string{$i})), 2, '0', STR_PAD_LEFT); + $returnstring .= str_pad(dechex(ord($string[$i])), 2, '0', STR_PAD_LEFT); } else { - $returnstring .= ' '.(preg_match("#[\x20-\x7E]#", $string{$i}) ? $string{$i} : '¤'); + $returnstring .= ' '.(preg_match("#[\x20-\x7E]#", $string[$i]) ? $string[$i] : '¤'); } if ($spaces) { $returnstring .= ' '; @@ -36,9 +50,14 @@ class getid3_lib return $returnstring; } + /** + * Truncates a floating-point number at the decimal point. + * + * @param float $floatnumber + * + * @return float|int returns int (if possible, otherwise float) + */ public static function trunc($floatnumber) { - // truncates a floating-point number at the decimal point - // returns int (if possible, otherwise float) if ($floatnumber >= 1) { $truncatednumber = floor($floatnumber); } elseif ($floatnumber <= -1) { @@ -52,7 +71,12 @@ class getid3_lib return $truncatednumber; } - + /** + * @param int|null $variable + * @param int $increment + * + * @return bool + */ public static function safe_inc(&$variable, $increment=1) { if (isset($variable)) { $variable += $increment; @@ -62,6 +86,11 @@ class getid3_lib return true; } + /** + * @param int|float $floatnum + * + * @return int|float + */ public static function CastAsInt($floatnum) { // convert to float if not already $floatnum = (float) $floatnum; @@ -77,6 +106,11 @@ class getid3_lib return $floatnum; } + /** + * @param int $num + * + * @return bool + */ public static function intValueSupported($num) { // check if integers are 64-bit static $hasINT64 = null; @@ -93,28 +127,55 @@ class getid3_lib return false; } - public static function DecimalizeFraction($fraction) { - list($numerator, $denominator) = explode('/', $fraction); - return $numerator / ($denominator ? $denominator : 1); + /** + * Perform a division, guarding against division by zero + * + * @param float|int $numerator + * @param float|int $denominator + * @param float|int $fallback + * @return float|int + */ + public static function SafeDiv($numerator, $denominator, $fallback = 0) { + return $denominator ? $numerator / $denominator : $fallback; } + /** + * @param string $fraction + * + * @return float + */ + public static function DecimalizeFraction($fraction) { + list($numerator, $denominator) = explode('/', $fraction); + return (int) $numerator / ($denominator ? $denominator : 1); + } + /** + * @param string $binarynumerator + * + * @return float + */ public static function DecimalBinary2Float($binarynumerator) { $numerator = self::Bin2Dec($binarynumerator); $denominator = self::Bin2Dec('1'.str_repeat('0', strlen($binarynumerator))); return ($numerator / $denominator); } - + /** + * @link http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html + * + * @param string $binarypointnumber + * @param int $maxbits + * + * @return array + */ public static function NormalizeBinaryPoint($binarypointnumber, $maxbits=52) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html if (strpos($binarypointnumber, '.') === false) { $binarypointnumber = '0.'.$binarypointnumber; - } elseif ($binarypointnumber{0} == '.') { + } elseif ($binarypointnumber[0] == '.') { $binarypointnumber = '0'.$binarypointnumber; } $exponent = 0; - while (($binarypointnumber{0} != '1') || (substr($binarypointnumber, 1, 1) != '.')) { + while (($binarypointnumber[0] != '1') || (substr($binarypointnumber, 1, 1) != '.')) { if (substr($binarypointnumber, 1, 1) == '.') { $exponent--; $binarypointnumber = substr($binarypointnumber, 2, 1).'.'.substr($binarypointnumber, 3); @@ -122,16 +183,21 @@ class getid3_lib $pointpos = strpos($binarypointnumber, '.'); $exponent += ($pointpos - 1); $binarypointnumber = str_replace('.', '', $binarypointnumber); - $binarypointnumber = $binarypointnumber{0}.'.'.substr($binarypointnumber, 1); + $binarypointnumber = $binarypointnumber[0].'.'.substr($binarypointnumber, 1); } } $binarypointnumber = str_pad(substr($binarypointnumber, 0, $maxbits + 2), $maxbits + 2, '0', STR_PAD_RIGHT); return array('normalized'=>$binarypointnumber, 'exponent'=>(int) $exponent); } - + /** + * @link http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html + * + * @param float $floatvalue + * + * @return string + */ public static function Float2BinaryDecimal($floatvalue) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html $maxbits = 128; // to how many bits of precision should the calculations be taken? $intpart = self::trunc($floatvalue); $floatpart = abs($floatvalue - $intpart); @@ -145,9 +211,17 @@ class getid3_lib return $binarypointnumber; } - + /** + * @link http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee-expl.html + * + * @param float $floatvalue + * @param int $bits + * + * @return string|false + */ public static function Float2String($floatvalue, $bits) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee-expl.html + $exponentbits = 0; + $fractionbits = 0; switch ($bits) { case 32: $exponentbits = 8; @@ -161,7 +235,6 @@ class getid3_lib default: return false; - break; } if ($floatvalue >= 0) { $signbit = '0'; @@ -176,22 +249,34 @@ class getid3_lib return self::BigEndian2String(self::Bin2Dec($signbit.$exponentbitstring.$fractionbitstring), $bits % 8, false); } - + /** + * @param string $byteword + * + * @return float|false + */ public static function LittleEndian2Float($byteword) { return self::BigEndian2Float(strrev($byteword)); } - + /** + * ANSI/IEEE Standard 754-1985, Standard for Binary Floating Point Arithmetic + * + * @link https://web.archive.org/web/20120325162206/http://www.psc.edu/general/software/packages/ieee/ieee.php + * @link http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee.html + * + * @param string $byteword + * + * @return float|false + */ public static function BigEndian2Float($byteword) { - // ANSI/IEEE Standard 754-1985, Standard for Binary Floating Point Arithmetic - // http://www.psc.edu/general/software/packages/ieee/ieee.html - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee.html - $bitword = self::BigEndian2Bin($byteword); if (!$bitword) { return 0; } - $signbit = $bitword{0}; + $signbit = $bitword[0]; + $floatvalue = 0; + $exponentbits = 0; + $fractionbits = 0; switch (strlen($byteword) * 8) { case 32: @@ -208,7 +293,7 @@ class getid3_lib // 80-bit Apple SANE format // http://www.mactech.com/articles/mactech/Vol.06/06.01/SANENormalized/ $exponentstring = substr($bitword, 1, 15); - $isnormalized = intval($bitword{16}); + $isnormalized = intval($bitword[16]); $fractionstring = substr($bitword, 17, 63); $exponent = pow(2, self::Bin2Dec($exponentstring) - 16383); $fraction = $isnormalized + self::DecimalBinary2Float($fractionstring); @@ -217,11 +302,9 @@ class getid3_lib $floatvalue *= -1; } return $floatvalue; - break; default: return false; - break; } $exponentstring = substr($bitword, 1, $exponentbits); $fractionstring = substr($bitword, $exponentbits + 1, $fractionbits); @@ -230,20 +313,19 @@ class getid3_lib if (($exponent == (pow(2, $exponentbits) - 1)) && ($fraction != 0)) { // Not a Number - $floatvalue = false; + $floatvalue = NAN; } elseif (($exponent == (pow(2, $exponentbits) - 1)) && ($fraction == 0)) { if ($signbit == '1') { - $floatvalue = '-infinity'; + $floatvalue = -INF; } else { - $floatvalue = '+infinity'; + $floatvalue = INF; } } elseif (($exponent == 0) && ($fraction == 0)) { if ($signbit == '1') { - $floatvalue = -0; + $floatvalue = -0.0; } else { - $floatvalue = 0; + $floatvalue = 0.0; } - $floatvalue = ($signbit ? 0 : -0); } elseif (($exponent == 0) && ($fraction != 0)) { // These are 'unnormalized' values $floatvalue = pow(2, (-1 * (pow(2, $exponentbits - 1) - 2))) * self::DecimalBinary2Float($fractionstring); @@ -259,7 +341,14 @@ class getid3_lib return (float) $floatvalue; } - + /** + * @param string $byteword + * @param bool $synchsafe + * @param bool $signed + * + * @return int|float|false + * @throws Exception + */ public static function BigEndian2Int($byteword, $synchsafe=false, $signed=false) { $intvalue = 0; $bytewordlen = strlen($byteword); @@ -269,9 +358,9 @@ class getid3_lib for ($i = 0; $i < $bytewordlen; $i++) { if ($synchsafe) { // disregard MSB, effectively 7-bit bytes //$intvalue = $intvalue | (ord($byteword{$i}) & 0x7F) << (($bytewordlen - 1 - $i) * 7); // faster, but runs into problems past 2^31 on 32-bit systems - $intvalue += (ord($byteword{$i}) & 0x7F) * pow(2, ($bytewordlen - 1 - $i) * 7); + $intvalue += (ord($byteword[$i]) & 0x7F) * pow(2, ($bytewordlen - 1 - $i) * 7); } else { - $intvalue += ord($byteword{$i}) * pow(256, ($bytewordlen - 1 - $i)); + $intvalue += ord($byteword[$i]) * pow(256, ($bytewordlen - 1 - $i)); } } if ($signed && !$synchsafe) { @@ -288,25 +377,48 @@ class getid3_lib return self::CastAsInt($intvalue); } - + /** + * @param string $byteword + * @param bool $signed + * + * @return int|float|false + */ public static function LittleEndian2Int($byteword, $signed=false) { return self::BigEndian2Int(strrev($byteword), false, $signed); } + /** + * @param string $byteword + * + * @return string + */ public static function LittleEndian2Bin($byteword) { return self::BigEndian2Bin(strrev($byteword)); } + /** + * @param string $byteword + * + * @return string + */ public static function BigEndian2Bin($byteword) { $binvalue = ''; $bytewordlen = strlen($byteword); for ($i = 0; $i < $bytewordlen; $i++) { - $binvalue .= str_pad(decbin(ord($byteword{$i})), 8, '0', STR_PAD_LEFT); + $binvalue .= str_pad(decbin(ord($byteword[$i])), 8, '0', STR_PAD_LEFT); } return $binvalue; } - + /** + * @param int $number + * @param int $minbytes + * @param bool $synchsafe + * @param bool $signed + * + * @return string + * @throws Exception + */ public static function BigEndian2String($number, $minbytes=1, $synchsafe=false, $signed=false) { if ($number < 0) { throw new Exception('ERROR: self::BigEndian2String() does not support negative numbers'); @@ -327,25 +439,40 @@ class getid3_lib return str_pad($intstring, $minbytes, "\x00", STR_PAD_LEFT); } - + /** + * @param int $number + * + * @return string + */ public static function Dec2Bin($number) { + if (!is_numeric($number)) { + // https://github.com/JamesHeinrich/getID3/issues/299 + trigger_error('TypeError: Dec2Bin(): Argument #1 ($number) must be numeric, '.gettype($number).' given', E_USER_WARNING); + return ''; + } + $bytes = array(); while ($number >= 256) { - $bytes[] = (($number / 256) - (floor($number / 256))) * 256; + $bytes[] = (int) (($number / 256) - (floor($number / 256))) * 256; $number = floor($number / 256); } - $bytes[] = $number; + $bytes[] = (int) $number; $binstring = ''; - for ($i = 0; $i < count($bytes); $i++) { - $binstring = (($i == count($bytes) - 1) ? decbin($bytes[$i]) : str_pad(decbin($bytes[$i]), 8, '0', STR_PAD_LEFT)).$binstring; + foreach ($bytes as $i => $byte) { + $binstring = (($i == count($bytes) - 1) ? decbin($byte) : str_pad(decbin($byte), 8, '0', STR_PAD_LEFT)).$binstring; } return $binstring; } - + /** + * @param string $binstring + * @param bool $signed + * + * @return int|float + */ public static function Bin2Dec($binstring, $signed=false) { $signmult = 1; if ($signed) { - if ($binstring{0} == '1') { + if ($binstring[0] == '1') { $signmult = -1; } $binstring = substr($binstring, 1); @@ -357,7 +484,11 @@ class getid3_lib return self::CastAsInt($decvalue * $signmult); } - + /** + * @param string $binstring + * + * @return string + */ public static function Bin2String($binstring) { // return 'hi' for input of '0110100001101001' $string = ''; @@ -368,7 +499,13 @@ class getid3_lib return $string; } - + /** + * @param int $number + * @param int $minbytes + * @param bool $synchsafe + * + * @return string + */ public static function LittleEndian2String($number, $minbytes=1, $synchsafe=false) { $intstring = ''; while ($number > 0) { @@ -383,7 +520,12 @@ class getid3_lib return str_pad($intstring, $minbytes, "\x00", STR_PAD_RIGHT); } - + /** + * @param mixed $array1 + * @param mixed $array2 + * + * @return array|false + */ public static function array_merge_clobber($array1, $array2) { // written by kcØhireability*com // taken from http://www.php.net/manual/en/function.array-merge-recursive.php @@ -401,7 +543,12 @@ class getid3_lib return $newarray; } - + /** + * @param mixed $array1 + * @param mixed $array2 + * + * @return array|false + */ public static function array_merge_noclobber($array1, $array2) { if (!is_array($array1) || !is_array($array2)) { return false; @@ -417,6 +564,12 @@ class getid3_lib return $newarray; } + /** + * @param mixed $array1 + * @param mixed $array2 + * + * @return array|false|null + */ public static function flipped_array_merge_noclobber($array1, $array2) { if (!is_array($array1) || !is_array($array2)) { return false; @@ -431,7 +584,11 @@ class getid3_lib return array_flip($newarray); } - + /** + * @param array $theArray + * + * @return bool + */ public static function ksort_recursive(&$theArray) { ksort($theArray); foreach ($theArray as $key => $value) { @@ -442,6 +599,12 @@ class getid3_lib return true; } + /** + * @param string $filename + * @param int $numextensions + * + * @return string + */ public static function fileextension($filename, $numextensions=1) { if (strstr($filename, '.')) { $reversedfilename = strrev($filename); @@ -457,7 +620,11 @@ class getid3_lib return ''; } - + /** + * @param int $seconds + * + * @return string + */ public static function PlaytimeString($seconds) { $sign = (($seconds < 0) ? '-' : ''); $seconds = round(abs($seconds)); @@ -467,30 +634,53 @@ class getid3_lib return $sign.($H ? $H.':' : '').($H ? str_pad($M, 2, '0', STR_PAD_LEFT) : intval($M)).':'.str_pad($S, 2, 0, STR_PAD_LEFT); } - + /** + * @param int $macdate + * + * @return int|float + */ public static function DateMac2Unix($macdate) { // Macintosh timestamp: seconds since 00:00h January 1, 1904 // UNIX timestamp: seconds since 00:00h January 1, 1970 return self::CastAsInt($macdate - 2082844800); } - + /** + * @param string $rawdata + * + * @return float + */ public static function FixedPoint8_8($rawdata) { return self::BigEndian2Int(substr($rawdata, 0, 1)) + (float) (self::BigEndian2Int(substr($rawdata, 1, 1)) / pow(2, 8)); } - + /** + * @param string $rawdata + * + * @return float + */ public static function FixedPoint16_16($rawdata) { return self::BigEndian2Int(substr($rawdata, 0, 2)) + (float) (self::BigEndian2Int(substr($rawdata, 2, 2)) / pow(2, 16)); } - + /** + * @param string $rawdata + * + * @return float + */ public static function FixedPoint2_30($rawdata) { $binarystring = self::BigEndian2Bin($rawdata); return self::Bin2Dec(substr($binarystring, 0, 2)) + (float) (self::Bin2Dec(substr($binarystring, 2, 30)) / pow(2, 30)); } + /** + * @param string $ArrayPath + * @param string $Separator + * @param mixed $Value + * + * @return array + */ public static function CreateDeepArray($ArrayPath, $Separator, $Value) { // assigns $Value to a nested array path: // $foo = self::CreateDeepArray('/path/to/my', '/', 'file.txt') @@ -499,6 +689,7 @@ class getid3_lib // or // $foo['path']['to']['my'] = 'file.txt'; $ArrayPath = ltrim($ArrayPath, $Separator); + $ReturnedArray = array(); if (($pos = strpos($ArrayPath, $Separator)) !== false) { $ReturnedArray[substr($ArrayPath, 0, $pos)] = self::CreateDeepArray(substr($ArrayPath, $pos + 1), $Separator, $Value); } else { @@ -507,12 +698,18 @@ class getid3_lib return $ReturnedArray; } + /** + * @param array $arraydata + * @param bool $returnkey + * + * @return int|false + */ public static function array_max($arraydata, $returnkey=false) { $maxvalue = false; - $maxkey = false; + $maxkey = false; foreach ($arraydata as $key => $value) { if (!is_array($value)) { - if ($value > $maxvalue) { + if (($maxvalue === false) || ($value > $maxvalue)) { $maxvalue = $value; $maxkey = $key; } @@ -521,12 +718,18 @@ class getid3_lib return ($returnkey ? $maxkey : $maxvalue); } + /** + * @param array $arraydata + * @param bool $returnkey + * + * @return int|false + */ public static function array_min($arraydata, $returnkey=false) { $minvalue = false; - $minkey = false; + $minkey = false; foreach ($arraydata as $key => $value) { if (!is_array($value)) { - if ($value > $minvalue) { + if (($minvalue === false) || ($value < $minvalue)) { $minvalue = $value; $minkey = $key; } @@ -535,119 +738,88 @@ class getid3_lib return ($returnkey ? $minkey : $minvalue); } + /** + * @param string $XMLstring + * + * @return array|false + */ public static function XML2array($XMLstring) { if (function_exists('simplexml_load_string') && function_exists('libxml_disable_entity_loader')) { // http://websec.io/2012/08/27/Preventing-XEE-in-PHP.html // https://core.trac.wordpress.org/changeset/29378 - $loader = libxml_disable_entity_loader(true); - $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', LIBXML_NOENT); + // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is + // disabled by default, but is still needed when LIBXML_NOENT is used. + $loader = @libxml_disable_entity_loader(true); + $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', GETID3_LIBXML_OPTIONS); $return = self::SimpleXMLelement2array($XMLobject); - libxml_disable_entity_loader($loader); + @libxml_disable_entity_loader($loader); return $return; } return false; } + /** + * @param SimpleXMLElement|array|mixed $XMLobject + * + * @return mixed + */ public static function SimpleXMLelement2array($XMLobject) { if (!is_object($XMLobject) && !is_array($XMLobject)) { return $XMLobject; } - $XMLarray = (is_object($XMLobject) ? get_object_vars($XMLobject) : $XMLobject); + $XMLarray = $XMLobject instanceof SimpleXMLElement ? get_object_vars($XMLobject) : $XMLobject; foreach ($XMLarray as $key => $value) { $XMLarray[$key] = self::SimpleXMLelement2array($value); } return $XMLarray; } - - // Allan Hansen - // self::md5_data() - returns md5sum for a file from startuing position to absolute end position + /** + * Returns checksum for a file from starting position to absolute end position. + * + * @param string $file + * @param int $offset + * @param int $end + * @param string $algorithm + * + * @return string|false + * @throws getid3_exception + */ public static function hash_data($file, $offset, $end, $algorithm) { - static $tempdir = ''; if (!self::intValueSupported($end)) { return false; } - switch ($algorithm) { - case 'md5': - $hash_function = 'md5_file'; - $unix_call = 'md5sum'; - $windows_call = 'md5sum.exe'; - $hash_length = 32; - break; - - case 'sha1': - $hash_function = 'sha1_file'; - $unix_call = 'sha1sum'; - $windows_call = 'sha1sum.exe'; - $hash_length = 40; - break; - - default: - throw new Exception('Invalid algorithm ('.$algorithm.') in self::hash_data()'); - break; + if (!in_array($algorithm, array('md5', 'sha1'))) { + throw new getid3_exception('Invalid algorithm ('.$algorithm.') in self::hash_data()'); } + $size = $end - $offset; - while (true) { - if (GETID3_OS_ISWINDOWS) { - // It seems that sha1sum.exe for Windows only works on physical files, does not accept piped data - // Fall back to create-temp-file method: - if ($algorithm == 'sha1') { - break; - } - - $RequiredFiles = array('cygwin1.dll', 'head.exe', 'tail.exe', $windows_call); - foreach ($RequiredFiles as $required_file) { - if (!is_readable(GETID3_HELPERAPPSDIR.$required_file)) { - // helper apps not available - fall back to old method - break 2; - } - } - $commandline = GETID3_HELPERAPPSDIR.'head.exe -c '.$end.' '.escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $file)).' | '; - $commandline .= GETID3_HELPERAPPSDIR.'tail.exe -c '.$size.' | '; - $commandline .= GETID3_HELPERAPPSDIR.$windows_call; - - } else { - - $commandline = 'head -c'.$end.' '.escapeshellarg($file).' | '; - $commandline .= 'tail -c'.$size.' | '; - $commandline .= $unix_call; - - } - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - //throw new Exception('PHP running in Safe Mode - backtick operator not available, using slower non-system-call '.$algorithm.' algorithm'); - break; - } - return substr(`$commandline`, 0, $hash_length); + $fp = fopen($file, 'rb'); + fseek($fp, $offset); + $ctx = hash_init($algorithm); + while ($size > 0) { + $buffer = fread($fp, min($size, getID3::FREAD_BUFFER_SIZE)); + hash_update($ctx, $buffer); + $size -= getID3::FREAD_BUFFER_SIZE; } + $hash = hash_final($ctx); + fclose($fp); - if (empty($tempdir)) { - // yes this is ugly, feel free to suggest a better way - require_once(dirname(__FILE__).'/getid3.php'); - $getid3_temp = new getID3(); - $tempdir = $getid3_temp->tempdir; - unset($getid3_temp); - } - // try to create a temporary file in the system temp directory - invalid dirname should force to system temp dir - if (($data_filename = tempnam($tempdir, 'gI3')) === false) { - // can't find anywhere to create a temp file, just fail - return false; - } - - // Init - $result = false; - - // copy parts of file - try { - self::CopyFileParts($file, $data_filename, $offset, $end - $offset); - $result = $hash_function($data_filename); - } catch (Exception $e) { - throw new Exception('self::CopyFileParts() failed in getid_lib::hash_data(): '.$e->getMessage()); - } - unlink($data_filename); - return $result; + return $hash; } + /** + * @param string $filename_source + * @param string $filename_dest + * @param int $offset + * @param int $length + * + * @return bool + * @throws Exception + * + * @deprecated Unused, may be removed in future versions of getID3 + */ public static function CopyFileParts($filename_source, $filename_dest, $offset, $length) { if (!self::intValueSupported($offset + $length)) { throw new Exception('cannot copy file portion, it extends beyond the '.round(PHP_INT_MAX / 1073741824).'GB limit'); @@ -660,21 +832,25 @@ class getid3_lib $byteswritten = fwrite($fp_dest, $buffer, $byteslefttowrite); $byteslefttowrite -= $byteswritten; } + fclose($fp_dest); return true; } else { + fclose($fp_src); throw new Exception('failed to seek to offset '.$offset.' in '.$filename_source); } - fclose($fp_dest); } else { throw new Exception('failed to create file for writing '.$filename_dest); } - fclose($fp_src); } else { throw new Exception('failed to open file for reading '.$filename_source); } - return false; } + /** + * @param int $charval + * + * @return string + */ public static function iconv_fallback_int_utf8($charval) { if ($charval < 128) { // 0bbbbbbb @@ -698,83 +874,108 @@ class getid3_lib return $newcharstring; } - // ISO-8859-1 => UTF-8 + /** + * ISO-8859-1 => UTF-8 + * + * @param string $string + * @param bool $bom + * + * @return string + */ public static function iconv_fallback_iso88591_utf8($string, $bom=false) { - if (function_exists('utf8_encode')) { - return utf8_encode($string); - } - // utf8_encode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) $newcharstring = ''; if ($bom) { $newcharstring .= "\xEF\xBB\xBF"; } for ($i = 0; $i < strlen($string); $i++) { - $charval = ord($string{$i}); + $charval = ord($string[$i]); $newcharstring .= self::iconv_fallback_int_utf8($charval); } return $newcharstring; } - // ISO-8859-1 => UTF-16BE + /** + * ISO-8859-1 => UTF-16BE + * + * @param string $string + * @param bool $bom + * + * @return string + */ public static function iconv_fallback_iso88591_utf16be($string, $bom=false) { $newcharstring = ''; if ($bom) { $newcharstring .= "\xFE\xFF"; } for ($i = 0; $i < strlen($string); $i++) { - $newcharstring .= "\x00".$string{$i}; + $newcharstring .= "\x00".$string[$i]; } return $newcharstring; } - // ISO-8859-1 => UTF-16LE + /** + * ISO-8859-1 => UTF-16LE + * + * @param string $string + * @param bool $bom + * + * @return string + */ public static function iconv_fallback_iso88591_utf16le($string, $bom=false) { $newcharstring = ''; if ($bom) { $newcharstring .= "\xFF\xFE"; } for ($i = 0; $i < strlen($string); $i++) { - $newcharstring .= $string{$i}."\x00"; + $newcharstring .= $string[$i]."\x00"; } return $newcharstring; } - // ISO-8859-1 => UTF-16LE (BOM) + /** + * ISO-8859-1 => UTF-16LE (BOM) + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_iso88591_utf16($string) { return self::iconv_fallback_iso88591_utf16le($string, true); } - // UTF-8 => ISO-8859-1 + /** + * UTF-8 => ISO-8859-1 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf8_iso88591($string) { - if (function_exists('utf8_decode')) { - return utf8_decode($string); - } - // utf8_decode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) $newcharstring = ''; $offset = 0; $stringlength = strlen($string); while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { + if ((ord($string[$offset]) | 0x07) == 0xF7) { // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x07) << 18) & + ((ord($string[($offset + 1)]) & 0x3F) << 12) & + ((ord($string[($offset + 2)]) & 0x3F) << 6) & + (ord($string[($offset + 3)]) & 0x3F); $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { + } elseif ((ord($string[$offset]) | 0x0F) == 0xEF) { // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x0F) << 12) & + ((ord($string[($offset + 1)]) & 0x3F) << 6) & + (ord($string[($offset + 2)]) & 0x3F); $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { + } elseif ((ord($string[$offset]) | 0x1F) == 0xDF) { // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x1F) << 6) & + (ord($string[($offset + 1)]) & 0x3F); $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { + } elseif ((ord($string[$offset]) | 0x7F) == 0x7F) { // 0bbbbbbb - $charval = ord($string{$offset}); + $charval = ord($string[$offset]); $offset += 1; } else { // error? throw some kind of warning here? @@ -788,7 +989,14 @@ class getid3_lib return $newcharstring; } - // UTF-8 => UTF-16BE + /** + * UTF-8 => UTF-16BE + * + * @param string $string + * @param bool $bom + * + * @return string + */ public static function iconv_fallback_utf8_utf16be($string, $bom=false) { $newcharstring = ''; if ($bom) { @@ -797,27 +1005,27 @@ class getid3_lib $offset = 0; $stringlength = strlen($string); while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { + if ((ord($string[$offset]) | 0x07) == 0xF7) { // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x07) << 18) & + ((ord($string[($offset + 1)]) & 0x3F) << 12) & + ((ord($string[($offset + 2)]) & 0x3F) << 6) & + (ord($string[($offset + 3)]) & 0x3F); $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { + } elseif ((ord($string[$offset]) | 0x0F) == 0xEF) { // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x0F) << 12) & + ((ord($string[($offset + 1)]) & 0x3F) << 6) & + (ord($string[($offset + 2)]) & 0x3F); $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { + } elseif ((ord($string[$offset]) | 0x1F) == 0xDF) { // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x1F) << 6) & + (ord($string[($offset + 1)]) & 0x3F); $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { + } elseif ((ord($string[$offset]) | 0x7F) == 0x7F) { // 0bbbbbbb - $charval = ord($string{$offset}); + $charval = ord($string[$offset]); $offset += 1; } else { // error? throw some kind of warning here? @@ -831,7 +1039,14 @@ class getid3_lib return $newcharstring; } - // UTF-8 => UTF-16LE + /** + * UTF-8 => UTF-16LE + * + * @param string $string + * @param bool $bom + * + * @return string + */ public static function iconv_fallback_utf8_utf16le($string, $bom=false) { $newcharstring = ''; if ($bom) { @@ -840,27 +1055,27 @@ class getid3_lib $offset = 0; $stringlength = strlen($string); while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { + if ((ord($string[$offset]) | 0x07) == 0xF7) { // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x07) << 18) & + ((ord($string[($offset + 1)]) & 0x3F) << 12) & + ((ord($string[($offset + 2)]) & 0x3F) << 6) & + (ord($string[($offset + 3)]) & 0x3F); $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { + } elseif ((ord($string[$offset]) | 0x0F) == 0xEF) { // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x0F) << 12) & + ((ord($string[($offset + 1)]) & 0x3F) << 6) & + (ord($string[($offset + 2)]) & 0x3F); $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { + } elseif ((ord($string[$offset]) | 0x1F) == 0xDF) { // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); + $charval = ((ord($string[($offset + 0)]) & 0x1F) << 6) & + (ord($string[($offset + 1)]) & 0x3F); $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { + } elseif ((ord($string[$offset]) | 0x7F) == 0x7F) { // 0bbbbbbb - $charval = ord($string{$offset}); + $charval = ord($string[$offset]); $offset += 1; } else { // error? maybe throw some warning here? @@ -874,12 +1089,24 @@ class getid3_lib return $newcharstring; } - // UTF-8 => UTF-16LE (BOM) + /** + * UTF-8 => UTF-16LE (BOM) + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf8_utf16($string) { return self::iconv_fallback_utf8_utf16le($string, true); } - // UTF-16BE => UTF-8 + /** + * UTF-16BE => UTF-8 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16be_utf8($string) { if (substr($string, 0, 2) == "\xFE\xFF") { // strip BOM @@ -893,7 +1120,13 @@ class getid3_lib return $newcharstring; } - // UTF-16LE => UTF-8 + /** + * UTF-16LE => UTF-8 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16le_utf8($string) { if (substr($string, 0, 2) == "\xFF\xFE") { // strip BOM @@ -907,7 +1140,13 @@ class getid3_lib return $newcharstring; } - // UTF-16BE => ISO-8859-1 + /** + * UTF-16BE => ISO-8859-1 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16be_iso88591($string) { if (substr($string, 0, 2) == "\xFE\xFF") { // strip BOM @@ -921,7 +1160,13 @@ class getid3_lib return $newcharstring; } - // UTF-16LE => ISO-8859-1 + /** + * UTF-16LE => ISO-8859-1 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16le_iso88591($string) { if (substr($string, 0, 2) == "\xFF\xFE") { // strip BOM @@ -935,7 +1180,13 @@ class getid3_lib return $newcharstring; } - // UTF-16 (BOM) => ISO-8859-1 + /** + * UTF-16 (BOM) => ISO-8859-1 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16_iso88591($string) { $bom = substr($string, 0, 2); if ($bom == "\xFE\xFF") { @@ -946,7 +1197,13 @@ class getid3_lib return $string; } - // UTF-16 (BOM) => UTF-8 + /** + * UTF-16 (BOM) => UTF-8 + * + * @param string $string + * + * @return string + */ public static function iconv_fallback_utf16_utf8($string) { $bom = substr($string, 0, 2); if ($bom == "\xFE\xFF") { @@ -957,14 +1214,32 @@ class getid3_lib return $string; } + /** + * @param string $in_charset + * @param string $out_charset + * @param string $string + * + * @return string + * @throws Exception + */ public static function iconv_fallback($in_charset, $out_charset, $string) { if ($in_charset == $out_charset) { return $string; } - // mb_convert_encoding() availble + // mb_convert_encoding() available if (function_exists('mb_convert_encoding')) { + if ((strtoupper($in_charset) == 'UTF-16') && (substr($string, 0, 2) != "\xFE\xFF") && (substr($string, 0, 2) != "\xFF\xFE")) { + // if BOM missing, mb_convert_encoding will mishandle the conversion, assume UTF-16BE and prepend appropriate BOM + $string = "\xFF\xFE".$string; + } + if ((strtoupper($in_charset) == 'UTF-16') && (strtoupper($out_charset) == 'UTF-8')) { + if (($string == "\xFF\xFE") || ($string == "\xFE\xFF")) { + // if string consists of only BOM, mb_convert_encoding will return the BOM unmodified + return ''; + } + } if ($converted_string = @mb_convert_encoding($string, $out_charset, $in_charset)) { switch ($out_charset) { case 'ISO-8859-1': @@ -974,9 +1249,9 @@ class getid3_lib return $converted_string; } return $string; - } - // iconv() availble - else if (function_exists('iconv')) { + + // iconv() available + } elseif (function_exists('iconv')) { if ($converted_string = @iconv($in_charset, $out_charset.'//TRANSLIT', $string)) { switch ($out_charset) { case 'ISO-8859-1': @@ -1017,6 +1292,12 @@ class getid3_lib throw new Exception('PHP does not has mb_convert_encoding() or iconv() support - cannot convert from '.$in_charset.' to '.$out_charset); } + /** + * @param mixed $data + * @param string $charset + * + * @return mixed + */ public static function recursiveMultiByteCharString2HTML($data, $charset='ISO-8859-1') { if (is_string($data)) { return self::MultiByteCharString2HTML($data, $charset); @@ -1031,6 +1312,12 @@ class getid3_lib return $data; } + /** + * @param string|int|float $string + * @param string $charset + * + * @return string + */ public static function MultiByteCharString2HTML($string, $charset='ISO-8859-1') { $string = (string) $string; // in case trying to pass a numeric (float, int) string, would otherwise return an empty string $HTMLstring = ''; @@ -1069,22 +1356,22 @@ class getid3_lib case 'utf-8': $strlen = strlen($string); for ($i = 0; $i < $strlen; $i++) { - $char_ord_val = ord($string{$i}); + $char_ord_val = ord($string[$i]); $charval = 0; if ($char_ord_val < 0x80) { $charval = $char_ord_val; } elseif ((($char_ord_val & 0xF0) >> 4) == 0x0F && $i+3 < $strlen) { $charval = (($char_ord_val & 0x07) << 18); - $charval += ((ord($string{++$i}) & 0x3F) << 12); - $charval += ((ord($string{++$i}) & 0x3F) << 6); - $charval += (ord($string{++$i}) & 0x3F); + $charval += ((ord($string[++$i]) & 0x3F) << 12); + $charval += ((ord($string[++$i]) & 0x3F) << 6); + $charval += (ord($string[++$i]) & 0x3F); } elseif ((($char_ord_val & 0xE0) >> 5) == 0x07 && $i+2 < $strlen) { $charval = (($char_ord_val & 0x0F) << 12); - $charval += ((ord($string{++$i}) & 0x3F) << 6); - $charval += (ord($string{++$i}) & 0x3F); + $charval += ((ord($string[++$i]) & 0x3F) << 6); + $charval += (ord($string[++$i]) & 0x3F); } elseif ((($char_ord_val & 0xC0) >> 6) == 0x03 && $i+1 < $strlen) { $charval = (($char_ord_val & 0x1F) << 6); - $charval += (ord($string{++$i}) & 0x3F); + $charval += (ord($string[++$i]) & 0x3F); } if (($charval >= 32) && ($charval <= 127)) { $HTMLstring .= htmlentities(chr($charval)); @@ -1123,8 +1410,11 @@ class getid3_lib return $HTMLstring; } - - + /** + * @param int $namecode + * + * @return string + */ public static function RGADnameLookup($namecode) { static $RGADname = array(); if (empty($RGADname)) { @@ -1136,7 +1426,11 @@ class getid3_lib return (isset($RGADname[$namecode]) ? $RGADname[$namecode] : ''); } - + /** + * @param int $originatorcode + * + * @return string + */ public static function RGADoriginatorLookup($originatorcode) { static $RGADoriginator = array(); if (empty($RGADoriginator)) { @@ -1149,16 +1443,27 @@ class getid3_lib return (isset($RGADoriginator[$originatorcode]) ? $RGADoriginator[$originatorcode] : ''); } - + /** + * @param int $rawadjustment + * @param int $signbit + * + * @return float + */ public static function RGADadjustmentLookup($rawadjustment, $signbit) { - $adjustment = $rawadjustment / 10; + $adjustment = (float) $rawadjustment / 10; if ($signbit == 1) { $adjustment *= -1; } - return (float) $adjustment; + return $adjustment; } - + /** + * @param int $namecode + * @param int $originatorcode + * @param int $replaygain + * + * @return string + */ public static function RGADgainString($namecode, $originatorcode, $replaygain) { if ($replaygain < 0) { $signbit = '1'; @@ -1174,12 +1479,31 @@ class getid3_lib return $gainstring; } + /** + * @param float $amplitude + * + * @return float + */ public static function RGADamplitude2dB($amplitude) { return 20 * log10($amplitude); } - + /** + * @param string $imgData + * @param array $imageinfo + * + * @return array|false + */ public static function GetDataImageSize($imgData, &$imageinfo=array()) { + if (PHP_VERSION_ID >= 50400) { + $GetDataImageSize = @getimagesizefromstring($imgData, $imageinfo); + if ($GetDataImageSize === false || !isset($GetDataImageSize[0], $GetDataImageSize[1])) { + return false; + } + $GetDataImageSize['height'] = $GetDataImageSize[0]; + $GetDataImageSize['width'] = $GetDataImageSize[1]; + return $GetDataImageSize; + } static $tempdir = ''; if (empty($tempdir)) { if (function_exists('sys_get_temp_dir')) { @@ -1188,12 +1512,11 @@ class getid3_lib // yes this is ugly, feel free to suggest a better way if (include_once(dirname(__FILE__).'/getid3.php')) { - if ($getid3_temp = new getID3()) { - if ($getid3_temp_tempdir = $getid3_temp->tempdir) { - $tempdir = $getid3_temp_tempdir; - } - unset($getid3_temp, $getid3_temp_tempdir); + $getid3_temp = new getID3(); + if ($getid3_temp_tempdir = $getid3_temp->tempdir) { + $tempdir = $getid3_temp_tempdir; } + unset($getid3_temp, $getid3_temp_tempdir); } } $GetDataImageSize = false; @@ -1213,36 +1536,41 @@ class getid3_lib return $GetDataImageSize; } + /** + * @param string $mime_type + * + * @return string + */ public static function ImageExtFromMime($mime_type) { // temporary way, works OK for now, but should be reworked in the future return str_replace(array('image/', 'x-', 'jpeg'), array('', '', 'jpg'), $mime_type); } - public static function ImageTypesLookup($imagetypeid) { - static $ImageTypesLookup = array(); - if (empty($ImageTypesLookup)) { - $ImageTypesLookup[1] = 'gif'; - $ImageTypesLookup[2] = 'jpeg'; - $ImageTypesLookup[3] = 'png'; - $ImageTypesLookup[4] = 'swf'; - $ImageTypesLookup[5] = 'psd'; - $ImageTypesLookup[6] = 'bmp'; - $ImageTypesLookup[7] = 'tiff (little-endian)'; - $ImageTypesLookup[8] = 'tiff (big-endian)'; - $ImageTypesLookup[9] = 'jpc'; - $ImageTypesLookup[10] = 'jp2'; - $ImageTypesLookup[11] = 'jpx'; - $ImageTypesLookup[12] = 'jb2'; - $ImageTypesLookup[13] = 'swc'; - $ImageTypesLookup[14] = 'iff'; - } - return (isset($ImageTypesLookup[$imagetypeid]) ? $ImageTypesLookup[$imagetypeid] : ''); - } - - public static function CopyTagsToComments(&$ThisFileInfo) { - + /** + * @param array $ThisFileInfo + * @param bool $option_tags_html default true (just as in the main getID3 class) + * + * @return bool + */ + public static function CopyTagsToComments(&$ThisFileInfo, $option_tags_html=true) { // Copy all entries from ['tags'] into common ['comments'] if (!empty($ThisFileInfo['tags'])) { + + // Some tag types can only support limited character sets and may contain data in non-standard encoding (usually ID3v1) + // and/or poorly-transliterated tag values that are also in tag formats that do support full-range character sets + // To make the output more user-friendly, process the potentially-problematic tag formats last to enhance the chance that + // the first entries in [comments] are the most correct and the "bad" ones (if any) come later. + // https://github.com/JamesHeinrich/getID3/issues/338 + $processLastTagTypes = array('id3v1','riff'); + foreach ($processLastTagTypes as $processLastTagType) { + if (isset($ThisFileInfo['tags'][$processLastTagType])) { + // bubble ID3v1 to the end, if present to aid in detecting bad ID3v1 encodings + $temp = $ThisFileInfo['tags'][$processLastTagType]; + unset($ThisFileInfo['tags'][$processLastTagType]); + $ThisFileInfo['tags'][$processLastTagType] = $temp; + unset($temp); + } + } foreach ($ThisFileInfo['tags'] as $tagtype => $tagarray) { foreach ($tagarray as $tagname => $tagdata) { foreach ($tagdata as $key => $value) { @@ -1260,16 +1588,32 @@ class getid3_lib // new value is identical but shorter-than (or equal-length to) one already in comments - skip break 2; } + + if (function_exists('mb_convert_encoding')) { + if (trim($value) == trim(substr(mb_convert_encoding($existingvalue, $ThisFileInfo['id3v1']['encoding'], $ThisFileInfo['encoding']), 0, 30))) { + // value stored in ID3v1 appears to be probably the multibyte value transliterated (badly) into ISO-8859-1 in ID3v1. + // As an example, Foobar2000 will do this if you tag a file with Chinese or Arabic or Cyrillic or something that doesn't fit into ISO-8859-1 the ID3v1 will consist of mostly "?" characters, one per multibyte unrepresentable character + break 2; + } + } } } elseif (!is_array($value)) { - $newvaluelength = strlen(trim($value)); + $newvaluelength = strlen(trim($value)); + $newvaluelengthMB = mb_strlen(trim($value)); foreach ($ThisFileInfo['comments'][$tagname] as $existingkey => $existingvalue) { - $oldvaluelength = strlen(trim($existingvalue)); + $oldvaluelength = strlen(trim($existingvalue)); + $oldvaluelengthMB = mb_strlen(trim($existingvalue)); + if (($newvaluelengthMB == $oldvaluelengthMB) && ($existingvalue == getid3_lib::iconv_fallback('UTF-8', 'ASCII', $value))) { + // https://github.com/JamesHeinrich/getID3/issues/338 + // check for tags containing extended characters that may have been forced into limited-character storage (e.g. UTF8 values into ASCII) + // which will usually display unrepresentable characters as "?" + $ThisFileInfo['comments'][$tagname][$existingkey] = trim($value); + break; + } if ((strlen($existingvalue) > 10) && ($newvaluelength > $oldvaluelength) && (substr(trim($value), 0, strlen($existingvalue)) == $existingvalue)) { $ThisFileInfo['comments'][$tagname][$existingkey] = trim($value); - //break 2; break; } } @@ -1280,7 +1624,7 @@ class getid3_lib if (!is_int($key) && !ctype_digit($key)) { $ThisFileInfo['comments'][$tagname][$key] = $value; } else { - if (isset($ThisFileInfo['comments'][$tagname])) { + if (!isset($ThisFileInfo['comments'][$tagname])) { $ThisFileInfo['comments'][$tagname] = array($value); } else { $ThisFileInfo['comments'][$tagname][] = $value; @@ -1293,30 +1637,34 @@ class getid3_lib } // attempt to standardize spelling of returned keys - $StandardizeFieldNames = array( - 'tracknumber' => 'track_number', - 'track' => 'track_number', - ); - foreach ($StandardizeFieldNames as $badkey => $goodkey) { - if (array_key_exists($badkey, $ThisFileInfo['comments']) && !array_key_exists($goodkey, $ThisFileInfo['comments'])) { - $ThisFileInfo['comments'][$goodkey] = $ThisFileInfo['comments'][$badkey]; - unset($ThisFileInfo['comments'][$badkey]); + if (!empty($ThisFileInfo['comments'])) { + $StandardizeFieldNames = array( + 'tracknumber' => 'track_number', + 'track' => 'track_number', + ); + foreach ($StandardizeFieldNames as $badkey => $goodkey) { + if (array_key_exists($badkey, $ThisFileInfo['comments']) && !array_key_exists($goodkey, $ThisFileInfo['comments'])) { + $ThisFileInfo['comments'][$goodkey] = $ThisFileInfo['comments'][$badkey]; + unset($ThisFileInfo['comments'][$badkey]); + } } } - // Copy to ['comments_html'] - if (!empty($ThisFileInfo['comments'])) { - foreach ($ThisFileInfo['comments'] as $field => $values) { - if ($field == 'picture') { - // pictures can take up a lot of space, and we don't need multiple copies of them - // let there be a single copy in [comments][picture], and not elsewhere - continue; - } - foreach ($values as $index => $value) { - if (is_array($value)) { - $ThisFileInfo['comments_html'][$field][$index] = $value; - } else { - $ThisFileInfo['comments_html'][$field][$index] = str_replace('�', '', self::MultiByteCharString2HTML($value, $ThisFileInfo['encoding'])); + if ($option_tags_html) { + // Copy ['comments'] to ['comments_html'] + if (!empty($ThisFileInfo['comments'])) { + foreach ($ThisFileInfo['comments'] as $field => $values) { + if ($field == 'picture') { + // pictures can take up a lot of space, and we don't need multiple copies of them + // let there be a single copy in [comments][picture], and not elsewhere + continue; + } + foreach ($values as $index => $value) { + if (is_array($value)) { + $ThisFileInfo['comments_html'][$field][$index] = $value; + } else { + $ThisFileInfo['comments_html'][$field][$index] = str_replace('�', '', self::MultiByteCharString2HTML($value, $ThisFileInfo['encoding'])); + } } } } @@ -1326,7 +1674,15 @@ class getid3_lib return true; } - + /** + * @param string $key + * @param int $begin + * @param int $end + * @param string $file + * @param string $name + * + * @return string + */ public static function EmbeddedLookup($key, $begin, $end, $file, $name) { // Cached @@ -1373,6 +1729,14 @@ class getid3_lib return (isset($cache[$file][$name][$key]) ? $cache[$file][$name][$key] : ''); } + /** + * @param string $filename + * @param string $sourcefile + * @param bool $DieOnFailure + * + * @return bool + * @throws Exception + */ public static function IncludeDependency($filename, $sourcefile, $DieOnFailure=false) { global $GETID3_ERRORARRAY; @@ -1393,11 +1757,22 @@ class getid3_lib return false; } + /** + * @param string $string + * + * @return string + */ public static function trimNullByte($string) { return trim($string, "\x00"); } + /** + * @param string $path + * + * @return float|bool + */ public static function getFileSizeSyscall($path) { + $commandline = null; $filesize = false; if (GETID3_OS_ISWINDOWS) { @@ -1421,14 +1796,45 @@ class getid3_lib return $filesize; } + /** + * @param string $filename + * + * @return string|false + */ + public static function truepath($filename) { + // 2017-11-08: this could use some improvement, patches welcome + if (preg_match('#^(\\\\\\\\|//)[a-z0-9]#i', $filename, $matches)) { + // PHP's built-in realpath function does not work on UNC Windows shares + $goodpath = array(); + foreach (explode('/', str_replace('\\', '/', $filename)) as $part) { + if ($part == '.') { + continue; + } + if ($part == '..') { + if (count($goodpath)) { + array_pop($goodpath); + } else { + // cannot step above this level, already at top level + return false; + } + } else { + $goodpath[] = $part; + } + } + return implode(DIRECTORY_SEPARATOR, $goodpath); + } + return realpath($filename); + } /** - * Workaround for Bug #37268 (https://bugs.php.net/bug.php?id=37268) - * @param string $path A path. - * @param string $suffix If the name component ends in suffix this will also be cut off. - * @return string - */ - public static function mb_basename($path, $suffix = null) { + * Workaround for Bug #37268 (https://bugs.php.net/bug.php?id=37268) + * + * @param string $path A path. + * @param string $suffix If the name component ends in suffix this will also be cut off. + * + * @return string + */ + public static function mb_basename($path, $suffix = '') { $splited = preg_split('#/#', rtrim($path, '/ ')); return substr(basename('X'.$splited[count($splited) - 1], $suffix), 1); } diff --git a/lib/getid3/getid3.php b/lib/getid3/getid3.php index 9fc1623..a821ec1 100644 --- a/lib/getid3/getid3.php +++ b/lib/getid3/getid3.php @@ -1,10 +1,9 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // // // // Please see readme.txt for more information // // /// @@ -18,16 +17,12 @@ if (!defined('GETID3_OS_ISWINDOWS')) { if (!defined('GETID3_INCLUDEPATH')) { define('GETID3_INCLUDEPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); } -// Workaround Bug #39923 (https://bugs.php.net/bug.php?id=39923) -if (!defined('IMG_JPG') && defined('IMAGETYPE_JPEG')) { - define('IMG_JPG', IMAGETYPE_JPEG); -} if (!defined('ENT_SUBSTITUTE')) { // PHP5.3 adds ENT_IGNORE, PHP5.4 adds ENT_SUBSTITUTE define('ENT_SUBSTITUTE', (defined('ENT_IGNORE') ? ENT_IGNORE : 8)); } /* -http://www.getid3.org/phpBB3/viewtopic.php?t=2114 +https://www.getid3.org/phpBB3/viewtopic.php?t=2114 If you are running into a the problem where filenames with special characters are being handled incorrectly by external helper programs (e.g. metaflac), notably with the special characters removed, and you are passing in the filename in UTF8 (typically via a HTML form), try uncommenting this line: @@ -58,7 +53,7 @@ if ($open_basedir) { if (substr($basedir, -1, 1) != DIRECTORY_SEPARATOR) { $basedir .= DIRECTORY_SEPARATOR; } - if (preg_match('#^'.preg_quote($basedir).'#', $temp_dir)) { + if (strpos($temp_dir, $basedir) === 0) { $found_valid_tempdir = true; break; } @@ -82,69 +77,345 @@ unset($open_basedir, $temp_dir); class getID3 { - // public: Settings - public $encoding = 'UTF-8'; // CASE SENSITIVE! - i.e. (must be supported by iconv()). Examples: ISO-8859-1 UTF-8 UTF-16 UTF-16BE - public $encoding_id3v1 = 'ISO-8859-1'; // Should always be 'ISO-8859-1', but some tags may be written in other encodings such as 'EUC-CN' or 'CP1252' + /* + * Settings + */ - // public: Optional tag checks - disable for speed. - public $option_tag_id3v1 = true; // Read and process ID3v1 tags - public $option_tag_id3v2 = true; // Read and process ID3v2 tags - public $option_tag_lyrics3 = true; // Read and process Lyrics3 tags - public $option_tag_apetag = true; // Read and process APE tags - public $option_tags_process = true; // Copy tags to root key 'tags' and encode to $this->encoding - public $option_tags_html = true; // Copy tags to root key 'tags_html' properly translated from various encodings to HTML entities + /** + * CASE SENSITIVE! - i.e. (must be supported by iconv()). Examples: ISO-8859-1 UTF-8 UTF-16 UTF-16BE + * + * @var string + */ + public $encoding = 'UTF-8'; - // public: Optional tag/comment calucations - public $option_extra_info = true; // Calculate additional info such as bitrate, channelmode etc + /** + * Should always be 'ISO-8859-1', but some tags may be written in other encodings such as 'EUC-CN' or 'CP1252' + * + * @var string + */ + public $encoding_id3v1 = 'ISO-8859-1'; - // public: Optional handling of embedded attachments (e.g. images) - public $option_save_attachments = true; // defaults to true (ATTACHMENTS_INLINE) for backward compatibility + /** + * ID3v1 should always be 'ISO-8859-1', but some tags may be written in other encodings such as 'Windows-1251' or 'KOI8-R'. If true attempt to detect these encodings, but may return incorrect values for some tags actually in ISO-8859-1 encoding + * + * @var bool + */ + public $encoding_id3v1_autodetect = false; - // public: Optional calculations - public $option_md5_data = false; // Get MD5 sum of data part - slow - public $option_md5_data_source = false; // Use MD5 of source file if availble - only FLAC and OptimFROG - public $option_sha1_data = false; // Get SHA1 sum of data part - slow - public $option_max_2gb_check = null; // Check whether file is larger than 2GB and thus not supported by 32-bit PHP (null: auto-detect based on PHP_INT_MAX) + /* + * Optional tag checks - disable for speed. + */ - // public: Read buffer size in bytes + /** + * Read and process ID3v1 tags + * + * @var bool + */ + public $option_tag_id3v1 = true; + + /** + * Read and process ID3v2 tags + * + * @var bool + */ + public $option_tag_id3v2 = true; + + /** + * Read and process Lyrics3 tags + * + * @var bool + */ + public $option_tag_lyrics3 = true; + + /** + * Read and process APE tags + * + * @var bool + */ + public $option_tag_apetag = true; + + /** + * Copy tags to root key 'tags' and encode to $this->encoding + * + * @var bool + */ + public $option_tags_process = true; + + /** + * Copy tags to root key 'tags_html' properly translated from various encodings to HTML entities + * + * @var bool + */ + public $option_tags_html = true; + + /* + * Optional tag/comment calculations + */ + + /** + * Calculate additional info such as bitrate, channelmode etc + * + * @var bool + */ + public $option_extra_info = true; + + /* + * Optional handling of embedded attachments (e.g. images) + */ + + /** + * Defaults to true (ATTACHMENTS_INLINE) for backward compatibility + * + * @var bool|string + */ + public $option_save_attachments = true; + + /* + * Optional calculations + */ + + /** + * Get MD5 sum of data part - slow + * + * @var bool + */ + public $option_md5_data = false; + + /** + * Use MD5 of source file if available - only FLAC and OptimFROG + * + * @var bool + */ + public $option_md5_data_source = false; + + /** + * Get SHA1 sum of data part - slow + * + * @var bool + */ + public $option_sha1_data = false; + + /** + * Check whether file is larger than 2GB and thus not supported by 32-bit PHP (null: auto-detect based on + * PHP_INT_MAX) + * + * @var bool|null + */ + public $option_max_2gb_check; + + /** + * Read buffer size in bytes + * + * @var int + */ public $option_fread_buffer_size = 32768; + + + // module-specific options + + /** archive.rar + * if true use PHP RarArchive extension, if false (non-extension parsing not yet written in getID3) + * + * @var bool + */ + public $options_archive_rar_use_php_rar_extension = true; + + /** archive.gzip + * Optional file list - disable for speed. + * Decode gzipped files, if possible, and parse recursively (.tar.gz for example). + * + * @var bool + */ + public $options_archive_gzip_parse_contents = false; + + /** audio.midi + * if false only parse most basic information, much faster for some files but may be inaccurate + * + * @var bool + */ + public $options_audio_midi_scanwholefile = true; + + /** audio.mp3 + * Forces getID3() to scan the file byte-by-byte and log all the valid audio frame headers - extremely slow, + * unrecommended, but may provide data from otherwise-unusable files. + * + * @var bool + */ + public $options_audio_mp3_allow_bruteforce = false; + + /** audio.mp3 + * number of frames to scan to determine if MPEG-audio sequence is valid + * Lower this number to 5-20 for faster scanning + * Increase this number to 50+ for most accurate detection of valid VBR/CBR mpeg-audio streams + * + * @var int + */ + public $options_audio_mp3_mp3_valid_check_frames = 50; + + /** audio.wavpack + * Avoid scanning all frames (break after finding ID_RIFF_HEADER and ID_CONFIG_BLOCK, + * significantly faster for very large files but other data may be missed + * + * @var bool + */ + public $options_audio_wavpack_quick_parsing = false; + + /** audio-video.flv + * Break out of the loop if too many frames have been scanned; only scan this + * many if meta frame does not contain useful duration. + * + * @var int + */ + public $options_audiovideo_flv_max_frames = 100000; + + /** audio-video.matroska + * If true, do not return information about CLUSTER chunks, since there's a lot of them + * and they're not usually useful [default: TRUE]. + * + * @var bool + */ + public $options_audiovideo_matroska_hide_clusters = true; + + /** audio-video.matroska + * True to parse the whole file, not only header [default: FALSE]. + * + * @var bool + */ + public $options_audiovideo_matroska_parse_whole_file = false; + + /** audio-video.quicktime + * return all parsed data from all atoms if true, otherwise just returned parsed metadata + * + * @var bool + */ + public $options_audiovideo_quicktime_ReturnAtomData = false; + + /** audio-video.quicktime + * return all parsed data from all atoms if true, otherwise just returned parsed metadata + * + * @var bool + */ + public $options_audiovideo_quicktime_ParseAllPossibleAtoms = false; + + /** audio-video.swf + * return all parsed tags if true, otherwise do not return tags not parsed by getID3 + * + * @var bool + */ + public $options_audiovideo_swf_ReturnAllTagData = false; + + /** graphic.bmp + * return BMP palette + * + * @var bool + */ + public $options_graphic_bmp_ExtractPalette = false; + + /** graphic.bmp + * return image data + * + * @var bool + */ + public $options_graphic_bmp_ExtractData = false; + + /** graphic.png + * If data chunk is larger than this do not read it completely (getID3 only needs the first + * few dozen bytes for parsing). + * + * @var int + */ + public $options_graphic_png_max_data_bytes = 10000000; + + /** misc.pdf + * return full details of PDF Cross-Reference Table (XREF) + * + * @var bool + */ + public $options_misc_pdf_returnXREF = false; + + /** misc.torrent + * Assume all .torrent files are less than 1MB and just read entire thing into memory for easy processing. + * Override this value if you need to process files larger than 1MB + * + * @var int + */ + public $options_misc_torrent_max_torrent_filesize = 1048576; + + + // Public variables - public $filename; // Filename of file being analysed. - public $fp; // Filepointer to file being analysed. - public $info; // Result array. + + /** + * Filename of file being analysed. + * + * @var string + */ + public $filename; + + /** + * Filepointer to file being analysed. + * + * @var resource + */ + public $fp; + + /** + * Result array. + * + * @var array + */ + public $info; + + /** + * @var string + */ public $tempdir = GETID3_TEMP_DIR; + + /** + * @var int + */ public $memory_limit = 0; - // Protected variables + /** + * @var string + */ protected $startup_error = ''; + + /** + * @var string + */ protected $startup_warning = ''; - const VERSION = '1.9.15-201709291043'; + const VERSION = '1.9.22-202207161647'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; const ATTACHMENTS_INLINE = true; - // public: constructor + /** + * @throws getid3_exception + */ public function __construct() { // Check for PHP version $required_php_version = '5.3.0'; if (version_compare(PHP_VERSION, $required_php_version, '<')) { $this->startup_error .= 'getID3() requires PHP v'.$required_php_version.' or higher - you are running v'.PHP_VERSION."\n"; - return false; + return; } // Check memory - $this->memory_limit = ini_get('memory_limit'); - if (preg_match('#([0-9]+) ?M#i', $this->memory_limit, $matches)) { + $memoryLimit = ini_get('memory_limit'); + if (preg_match('#([0-9]+) ?M#i', $memoryLimit, $matches)) { // could be stored as "16M" rather than 16777216 for example - $this->memory_limit = $matches[1] * 1048576; - } elseif (preg_match('#([0-9]+) ?G#i', $this->memory_limit, $matches)) { // The 'G' modifier is available since PHP 5.1.0 + $memoryLimit = $matches[1] * 1048576; + } elseif (preg_match('#([0-9]+) ?G#i', $memoryLimit, $matches)) { // The 'G' modifier is available since PHP 5.1.0 // could be stored as "2G" rather than 2147483648 for example - $this->memory_limit = $matches[1] * 1073741824; + $memoryLimit = $matches[1] * 1073741824; } + $this->memory_limit = $memoryLimit; + if ($this->memory_limit <= 0) { // memory limits probably disabled } elseif ($this->memory_limit <= 4194304) { @@ -158,26 +429,24 @@ class getID3 $this->warning('WARNING: Safe mode is on, shorten support disabled, md5data/sha1data for ogg vorbis disabled, ogg vorbos/flac tag writing disabled.'); } - if (($mbstring_func_overload = ini_get('mbstring.func_overload')) && ($mbstring_func_overload & 0x02)) { + if (($mbstring_func_overload = (int) ini_get('mbstring.func_overload')) && ($mbstring_func_overload & 0x02)) { // http://php.net/manual/en/mbstring.overload.php // "mbstring.func_overload in php.ini is a positive value that represents a combination of bitmasks specifying the categories of functions to be overloaded. It should be set to 1 to overload the mail() function. 2 for string functions, 4 for regular expression functions" // getID3 cannot run when string functions are overloaded. It doesn't matter if mail() or ereg* functions are overloaded since getID3 does not use those. $this->startup_error .= 'WARNING: php.ini contains "mbstring.func_overload = '.ini_get('mbstring.func_overload').'", getID3 cannot run with this setting (bitmask 2 (string functions) cannot be set). Recommended to disable entirely.'."\n"; } - // Check for magic_quotes_runtime - if (version_compare(PHP_VERSION, '7.4.0') < 0) { + // check for magic quotes in PHP < 5.4.0 (when these options were removed and getters always return false) + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + // Check for magic_quotes_runtime if (function_exists('get_magic_quotes_runtime')) { - if (get_magic_quotes_runtime()) { + if (get_magic_quotes_runtime()) { // @phpstan-ignore-line $this->startup_error .= 'magic_quotes_runtime must be disabled before running getID3(). Surround getid3 block by set_magic_quotes_runtime(0) and set_magic_quotes_runtime(1).'."\n"; } } - } - - // Check for magic_quotes_gpc - if (version_compare(PHP_VERSION, '7.4.0') < 0) { - if (function_exists('magic_quotes_gpc')) { - if (get_magic_quotes_gpc()) { + // Check for magic_quotes_gpc + if (function_exists('get_magic_quotes_gpc')) { + if (get_magic_quotes_gpc()) { // @phpstan-ignore-line $this->startup_error .= 'magic_quotes_gpc must be disabled before running getID3(). Surround getid3 block by set_magic_quotes_gpc(0) and set_magic_quotes_gpc(1).'."\n"; } } @@ -195,7 +464,7 @@ class getID3 // Needed for Windows only: // Define locations of helper applications for Shorten, VorbisComment, MetaFLAC - // as well as other helper functions such as head, tail, md5sum, etc + // as well as other helper functions such as head, etc // This path cannot contain spaces, but the below code will attempt to get the // 8.3-equivalent path automatically // IMPORTANT: This path must include the trailing slash @@ -238,20 +507,27 @@ class getID3 echo $this->startup_error; throw new getid3_exception($this->startup_error); } - - return true; } + /** + * @return string + */ public function version() { return self::VERSION; } + /** + * @return int + */ public function fread_buffer_size() { return $this->option_fread_buffer_size; } - - // public: setOption + /** + * @param array $optArray + * + * @return bool + */ public function setOption($optArray) { if (!is_array($optArray) || empty($optArray)) { return false; @@ -265,8 +541,16 @@ class getID3 return true; } - - public function openfile($filename, $filesize=null) { + /** + * @param string $filename + * @param int $filesize + * @param resource $fp + * + * @return bool + * + * @throws getid3_exception + */ + public function openfile($filename, $filesize=null, $fp=null) { try { if (!empty($this->startup_error)) { throw new getid3_exception($this->startup_error); @@ -284,16 +568,18 @@ class getID3 $this->info['php_memory_limit'] = (($this->memory_limit > 0) ? $this->memory_limit : false); // remote files not supported - if (preg_match('#^(ht|f)tp://#', $filename)) { + if (preg_match('#^(ht|f)tps?://#', $filename)) { throw new getid3_exception('Remote files are not supported - please copy the file locally first'); } $filename = str_replace('/', DIRECTORY_SEPARATOR, $filename); - $filename = preg_replace('#(?fp = fopen($filename, 'rb'))) { // see http://www.getid3.org/phpBB3/viewtopic.php?t=1720 - if ((is_readable($filename) || file_exists($filename)) && is_file($filename) && ($this->fp = fopen($filename, 'rb'))) { + //if (is_readable($filename) && is_file($filename) && ($this->fp = fopen($filename, 'rb'))) { // see https://www.getid3.org/phpBB3/viewtopic.php?t=1720 + if (($fp != null) && ((get_resource_type($fp) == 'file') || (get_resource_type($fp) == 'stream'))) { + $this->fp = $fp; + } elseif ((is_readable($filename) || file_exists($filename)) && is_file($filename) && ($this->fp = fopen($filename, 'rb'))) { // great } else { $errormessagelist = array(); @@ -350,10 +636,10 @@ class getID3 } elseif (getid3_lib::intValueSupported($real_filesize)) { unset($this->info['filesize']); fclose($this->fp); - throw new getid3_exception('PHP seems to think the file is larger than '.round(PHP_INT_MAX / 1073741824).'GB, but filesystem reports it as '.number_format($real_filesize, 3).'GB, please report to info@getid3.org'); + throw new getid3_exception('PHP seems to think the file is larger than '.round(PHP_INT_MAX / 1073741824).'GB, but filesystem reports it as '.number_format($real_filesize / 1073741824, 3).'GB, please report to info@getid3.org'); } $this->info['filesize'] = $real_filesize; - $this->warning('File is larger than '.round(PHP_INT_MAX / 1073741824).'GB (filesystem reports it as '.number_format($real_filesize, 3).'GB) and is not properly supported by PHP.'); + $this->warning('File is larger than '.round(PHP_INT_MAX / 1073741824).'GB (filesystem reports it as '.number_format($real_filesize / 1073741824, 3).'GB) and is not properly supported by PHP.'); } } @@ -365,10 +651,19 @@ class getID3 return false; } - // public: analyze file - public function analyze($filename, $filesize=null, $original_filename='') { + /** + * analyze file + * + * @param string $filename + * @param int $filesize + * @param string $original_filename + * @param resource $fp + * + * @return array + */ + public function analyze($filename, $filesize=null, $original_filename='', $fp=null) { try { - if (!$this->openfile($filename, $filesize)) { + if (!$this->openfile($filename, $filesize, $fp)) { return $this->info; } @@ -402,8 +697,8 @@ class getID3 $header = fread($this->fp, 10); if ((substr($header, 0, 3) == 'ID3') && (strlen($header) == 10)) { $this->info['id3v2']['header'] = true; - $this->info['id3v2']['majorversion'] = ord($header{3}); - $this->info['id3v2']['minorversion'] = ord($header{4}); + $this->info['id3v2']['majorversion'] = ord($header[3]); + $this->info['id3v2']['minorversion'] = ord($header[4]); $this->info['avdataoffset'] += getid3_lib::BigEndian2Int(substr($header, 6, 4), 1) + 10; // length of ID3v2 tag in 10-byte header doesn't include 10-byte header length } } @@ -471,6 +766,18 @@ class getID3 return $this->error('Format not supported, module "'.$determined_format['include'].'" is corrupt.'); } $class = new $class_name($this); + + // set module-specific options + foreach (get_object_vars($this) as $getid3_object_vars_key => $getid3_object_vars_value) { + if (preg_match('#^options_([^_]+)_([^_]+)_(.+)$#i', $getid3_object_vars_key, $matches)) { + list($dummy, $GOVgroup, $GOVmodule, $GOVsetting) = $matches; + $GOVgroup = (($GOVgroup == 'audiovideo') ? 'audio-video' : $GOVgroup); // variable names can only contain 0-9a-z_ so standardize here + if (($GOVgroup == $determined_format['group']) && ($GOVmodule == $determined_format['module'])) { + $class->$GOVsetting = $getid3_object_vars_value; + } + } + } + $class->Analyze(); unset($class); @@ -516,7 +823,13 @@ class getID3 } - // private: error handling + /** + * Error handling. + * + * @param string $message + * + * @return array + */ public function error($message) { $this->CleanUp(); if (!isset($this->info['error'])) { @@ -527,14 +840,22 @@ class getID3 } - // private: warning handling + /** + * Warning handling. + * + * @param string $message + * + * @return bool + */ public function warning($message) { $this->info['warning'][] = $message; return true; } - // private: CleanUp + /** + * @return bool + */ private function CleanUp() { // remove possible empty keys @@ -581,8 +902,11 @@ class getID3 return true; } - - // return array containing information about all supported formats + /** + * Return array containing information about all supported formats. + * + * @return array + */ public function GetFileFormatArray() { static $format_info = array(); if (empty($format_info)) { @@ -668,12 +992,20 @@ class getID3 // DSS - audio - Digital Speech Standard 'dss' => array( - 'pattern' => '^[\\x02-\\x06]ds[s2]', + 'pattern' => '^[\\x02-\\x08]ds[s2]', 'group' => 'audio', 'module' => 'dss', 'mime_type' => 'application/octet-stream', ), + // DSDIFF - audio - Direct Stream Digital Interchange File Format + 'dsdiff' => array( + 'pattern' => '^FRM8', + 'group' => 'audio', + 'module' => 'dsdiff', + 'mime_type' => 'audio/dsd', + ), + // DTS - audio - Dolby Theatre System 'dts' => array( 'pattern' => '^\\x7F\\xFE\\x80\\x01', @@ -687,7 +1019,7 @@ class getID3 'pattern' => '^fLaC', 'group' => 'audio', 'module' => 'flac', - 'mime_type' => 'audio/x-flac', + 'mime_type' => 'audio/flac', ), // LA - audio - Lossless Audio (LA) @@ -722,15 +1054,16 @@ class getID3 'mime_type' => 'audio/x-monkeys-audio', ), -// has been known to produce false matches in random files (e.g. JPEGs), leave out until more precise matching available -// // MOD - audio - MODule (assorted sub-formats) -// 'mod' => array( -// 'pattern' => '^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)', -// 'group' => 'audio', -// 'module' => 'mod', -// 'option' => 'mod', -// 'mime_type' => 'audio/mod', -// ), + + // MOD - audio - MODule (SoundTracker) + 'mod' => array( + //'pattern' => '^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)', // has been known to produce false matches in random files (e.g. JPEGs), leave out until more precise matching available + 'pattern' => '^.{1080}(M\\.K\\.)', + 'group' => 'audio', + 'module' => 'mod', + 'option' => 'mod', + 'mime_type' => 'audio/mod', + ), // MOD - audio - MODule (Impulse Tracker) 'it' => array( @@ -761,7 +1094,7 @@ class getID3 // MPC - audio - Musepack / MPEGplus 'mpc' => array( - 'pattern' => '^(MPCK|MP\\+|[\\x00\\x01\\x10\\x11\\x40\\x41\\x50\\x51\\x80\\x81\\x90\\x91\\xC0\\xC1\\xD0\\xD1][\\x20-\\x37][\\x00\\x20\\x40\\x60\\x80\\xA0\\xC0\\xE0])', + 'pattern' => '^(MPCK|MP\\+)', 'group' => 'audio', 'module' => 'mpc', 'mime_type' => 'audio/x-musepack', @@ -801,6 +1134,14 @@ class getID3 'fail_ape' => 'ERROR', ), + // TAK - audio - Tom's lossless Audio Kompressor + 'tak' => array( + 'pattern' => '^tBaK', + 'group' => 'audio', + 'module' => 'tak', + 'mime_type' => 'application/octet-stream', + ), + // TTA - audio - TTA Lossless Audio Compressor (http://tta.corecodec.org) 'tta' => array( 'pattern' => '^TTA', // could also be '^TTA(\\x01|\\x02|\\x03|2|1)' @@ -861,6 +1202,14 @@ class getID3 'mime_type' => 'video/x-flv', ), + // IVF - audio/video - IVF + 'ivf' => array( + 'pattern' => '^DKIF', + 'group' => 'audio-video', + 'module' => 'ivf', + 'mime_type' => 'video/x-ivf', + ), + // MKAV - audio/video - Mastroka 'matroska' => array( 'pattern' => '^\\x1A\\x45\\xDF\\xA3', @@ -908,7 +1257,7 @@ class getID3 'pattern' => '^(RIFF|SDSS|FORM)', 'group' => 'audio-video', 'module' => 'riff', - 'mime_type' => 'audio/x-wav', + 'mime_type' => 'audio/wav', 'fail_ape' => 'WARNING', ), @@ -936,6 +1285,14 @@ class getID3 'mime_type' => 'video/MP2T', ), + // WTV - audio/video - Windows Recorded TV Show + 'wtv' => array( + 'pattern' => '^\\xB7\\xD8\\x00\\x20\\x37\\x49\\xDA\\x11\\xA6\\x4E\\x00\\x07\\xE9\\x5E\\xAD\\x8D', + 'group' => 'audio-video', + 'module' => 'wtv', + 'mime_type' => 'video/x-ms-wtv', + ), + // Still-Image formats @@ -1037,12 +1394,22 @@ class getID3 'iconv_req' => false, ), + // HPK - data - HPK compressed data + 'hpk' => array( + 'pattern' => '^BPUL', + 'group' => 'archive', + 'module' => 'hpk', + 'mime_type' => 'application/octet-stream', + 'fail_id3' => 'ERROR', + 'fail_ape' => 'ERROR', + ), + // RAR - data - RAR compressed data 'rar' => array( 'pattern' => '^Rar\\!', 'group' => 'archive', 'module' => 'rar', - 'mime_type' => 'application/octet-stream', + 'mime_type' => 'application/vnd.rar', 'fail_id3' => 'ERROR', 'fail_ape' => 'ERROR', ), @@ -1072,7 +1439,7 @@ class getID3 'pattern' => '^\\x1F\\x8B\\x08', 'group' => 'archive', 'module' => 'gzip', - 'mime_type' => 'application/x-gzip', + 'mime_type' => 'application/gzip', 'fail_id3' => 'ERROR', 'fail_ape' => 'ERROR', ), @@ -1087,6 +1454,16 @@ class getID3 'fail_ape' => 'ERROR', ), + // XZ - data - XZ compressed data + 'xz' => array( + 'pattern' => '^\\xFD7zXZ\\x00', + 'group' => 'archive', + 'module' => 'xz', + 'mime_type' => 'application/x-xz', + 'fail_id3' => 'ERROR', + 'fail_ape' => 'ERROR', + ), + // Misc other formats @@ -1120,6 +1497,16 @@ class getID3 'fail_ape' => 'ERROR', ), + // TORRENT - .torrent + 'torrent' => array( + 'pattern' => '^(d8\\:announce|d7\\:comment)', + 'group' => 'misc', + 'module' => 'torrent', + 'mime_type' => 'application/x-bittorrent', + 'fail_id3' => 'ERROR', + 'fail_ape' => 'ERROR', + ), + // CUE - data - CUEsheet (index to single-file disc images) 'cue' => array( 'pattern' => '', // empty pattern means cannot be automatically detected, will fall through all other formats and match based on filename and very basic file contents @@ -1134,8 +1521,12 @@ class getID3 return $format_info; } - - + /** + * @param string $filedata + * @param string $filename + * + * @return mixed|false + */ public function GetFileFormat(&$filedata, $filename='') { // this function will determine the format of a file based on usually // the first 2-4 bytes of the file (8 bytes for PNG, 16 bytes for JPG, @@ -1154,12 +1545,19 @@ class getID3 if (preg_match('#\\.mp[123a]$#i', $filename)) { - // Too many mp3 encoders on the market put gabage in front of mpeg files + // Too many mp3 encoders on the market put garbage in front of mpeg files // use assume format on these if format detection failed $GetFileFormatArray = $this->GetFileFormatArray(); $info = $GetFileFormatArray['mp3']; $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; return $info; + } elseif (preg_match('#\\.mp[cp\\+]$#i', $filename) && preg_match('#[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]#s', $filedata)) { + // old-format (SV4-SV6) Musepack header that has a very loose pattern match and could falsely match other data (e.g. corrupt mp3) + // only enable this pattern check if the filename ends in .mpc/mpp/mp+ + $GetFileFormatArray = $this->GetFileFormatArray(); + $info = $GetFileFormatArray['mpc']; + $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; + return $info; } elseif (preg_match('#\\.cue$#i', $filename) && preg_match('#FILE "[^"]+" (BINARY|MOTOROLA|AIFF|WAVE|MP3)#', $filedata)) { // there's not really a useful consistent "magic" at the beginning of .cue files to identify them // so until I think of something better, just go by filename if all other format checks fail @@ -1173,8 +1571,12 @@ class getID3 return false; } - - // converts array to $encoding charset from $this->encoding + /** + * Converts array to $encoding charset from $this->encoding. + * + * @param array $array + * @param string $encoding + */ public function CharConvert(&$array, $encoding) { // identical encoding - end here @@ -1197,7 +1599,9 @@ class getID3 } } - + /** + * @return bool + */ public function HandleAllTags() { // key name => array (tag name, character encoding) @@ -1224,6 +1628,7 @@ class getID3 'flac' => array('vorbiscomment' , 'UTF-8'), 'divxtag' => array('divx' , 'ISO-8859-1'), 'iptc' => array('iptc' , 'ISO-8859-1'), + 'dsdiff' => array('dsdiff' , 'ISO-8859-1'), ); } @@ -1243,7 +1648,7 @@ class getID3 if (is_string($value)) { $value = trim($value, " \r\n\t"); // do not trim nulls from $value!! Unicode characters will get mangled if trailing nulls are removed! } - if ($value) { + if (isset($value) && $value !== "") { if (!is_numeric($key)) { $this->info['tags'][trim($tag_name)][trim($tag_key)][$key] = $value; } else { @@ -1252,6 +1657,7 @@ class getID3 } } if ($tag_key == 'picture') { + // pictures can take up a lot of space, and we don't need multiple copies of them; let there be a single copy in [comments][picture], and not elsewhere unset($this->info[$comment_name]['comments'][$tag_key]); } } @@ -1265,6 +1671,11 @@ class getID3 if ($this->option_tags_html) { foreach ($this->info['tags'][$tag_name] as $tag_key => $valuearray) { + if ($tag_key == 'picture') { + // Do not to try to convert binary picture data to HTML + // https://github.com/JamesHeinrich/getID3/issues/178 + continue; + } $this->info['tags_html'][$tag_name][$tag_key] = getid3_lib::recursiveMultiByteCharString2HTML($valuearray, $this->info[$comment_name]['encoding']); } } @@ -1273,8 +1684,7 @@ class getID3 } - // pictures can take up a lot of space, and we don't need multiple copies of them - // let there be a single copy in [comments][picture], and not elsewhere + // pictures can take up a lot of space, and we don't need multiple copies of them; let there be a single copy in [comments][picture], and not elsewhere if (!empty($this->info['tags'])) { $unset_keys = array('tags', 'tags_html'); foreach ($this->info['tags'] as $tagtype => $tagarray) { @@ -1320,6 +1730,22 @@ class getID3 return true; } + /** + * Calls getid3_lib::CopyTagsToComments() but passes in the option_tags_html setting from this instance of getID3 + * + * @param array $ThisFileInfo + * + * @return bool + */ + public function CopyTagsToComments(&$ThisFileInfo) { + return getid3_lib::CopyTagsToComments($ThisFileInfo, $this->option_tags_html); + } + + /** + * @param string $algorithm + * + * @return array|bool + */ public function getHashdata($algorithm) { switch ($algorithm) { case 'md5': @@ -1328,7 +1754,6 @@ class getID3 default: return $this->error('bad algorithm "'.$algorithm.'" in getHashdata()'); - break; } if (!empty($this->info['fileformat']) && !empty($this->info['dataformat']) && ($this->info['fileformat'] == 'ogg') && ($this->info['audio']['dataformat'] == 'vorbis')) { @@ -1442,7 +1867,6 @@ class getID3 return true; } - public function ChannelsBitratePlaytimeCalculations() { // set channelmode on audio @@ -1507,7 +1931,9 @@ class getID3 } } - + /** + * @return bool + */ public function CalculateCompressionRatioVideo() { if (empty($this->info['video'])) { return false; @@ -1551,11 +1977,13 @@ class getID3 } $BitrateUncompressed = $this->info['video']['resolution_x'] * $this->info['video']['resolution_y'] * $this->info['video']['bits_per_sample'] * $FrameRate; - $this->info['video']['compression_ratio'] = $BitrateCompressed / $BitrateUncompressed; + $this->info['video']['compression_ratio'] = getid3_lib::SafeDiv($BitrateCompressed, $BitrateUncompressed, 1); return true; } - + /** + * @return bool + */ public function CalculateCompressionRatioAudio() { if (empty($this->info['audio']['bitrate']) || empty($this->info['audio']['channels']) || empty($this->info['audio']['sample_rate']) || !is_numeric($this->info['audio']['sample_rate'])) { return false; @@ -1572,11 +2000,13 @@ class getID3 return true; } - + /** + * @return bool + */ public function CalculateReplayGain() { if (isset($this->info['replay_gain'])) { if (!isset($this->info['replay_gain']['reference_volume'])) { - $this->info['replay_gain']['reference_volume'] = (double) 89.0; + $this->info['replay_gain']['reference_volume'] = 89.0; } if (isset($this->info['replay_gain']['track']['adjustment'])) { $this->info['replay_gain']['track']['volume'] = $this->info['replay_gain']['reference_volume'] - $this->info['replay_gain']['track']['adjustment']; @@ -1595,6 +2025,9 @@ class getID3 return true; } + /** + * @return bool + */ public function ProcessAudioStreams() { if (!empty($this->info['audio']['bitrate']) || !empty($this->info['audio']['channels']) || !empty($this->info['audio']['sample_rate'])) { if (!isset($this->info['audio']['streams'])) { @@ -1608,10 +2041,20 @@ class getID3 return true; } + /** + * @return string|bool + */ public function getid3_tempnam() { return tempnam($this->tempdir, 'gI3'); } + /** + * @param string $name + * + * @return bool + * + * @throws getid3_exception + */ public function include_module($name) { //if (!file_exists($this->include_path.'module.'.$name.'.php')) { if (!file_exists(GETID3_INCLUDEPATH.'module.'.$name.'.php')) { @@ -1621,35 +2064,70 @@ class getID3 return true; } - public static function is_writable ($filename) { - $ret = is_writable($filename); - - if (!$ret) { - $perms = fileperms($filename); - $ret = ($perms & 0x0080) || ($perms & 0x0010) || ($perms & 0x0002); - } - - return $ret; - } + /** + * @param string $filename + * + * @return bool + */ + public static function is_writable ($filename) { + $ret = is_writable($filename); + if (!$ret) { + $perms = fileperms($filename); + $ret = ($perms & 0x0080) || ($perms & 0x0010) || ($perms & 0x0002); + } + return $ret; + } } -abstract class getid3_handler { +abstract class getid3_handler +{ /** * @var getID3 */ protected $getid3; // pointer - protected $data_string_flag = false; // analyzing filepointer or string - protected $data_string = ''; // string to analyze - protected $data_string_position = 0; // seek position in string - protected $data_string_length = 0; // string length + /** + * Analyzing filepointer or string. + * + * @var bool + */ + protected $data_string_flag = false; - private $dependency_to = null; + /** + * String to analyze. + * + * @var string + */ + protected $data_string = ''; + /** + * Seek position in string. + * + * @var int + */ + protected $data_string_position = 0; + /** + * String length. + * + * @var int + */ + protected $data_string_length = 0; + + /** + * @var string + */ + private $dependency_to; + + /** + * getid3_handler constructor. + * + * @param getID3 $getid3 + * @param string $call_module + */ public function __construct(getID3 $getid3, $call_module=null) { $this->getid3 = $getid3; @@ -1658,12 +2136,18 @@ abstract class getid3_handler { } } - - // Analyze from file pointer + /** + * Analyze from file pointer. + * + * @return bool + */ abstract public function Analyze(); - - // Analyze from string instead + /** + * Analyze from string instead. + * + * @param string $string + */ public function AnalyzeString($string) { // Enter string mode $this->setStringMode($string); @@ -1689,12 +2173,20 @@ abstract class getid3_handler { $this->data_string_flag = false; } + /** + * @param string $string + */ public function setStringMode($string) { $this->data_string_flag = true; $this->data_string = $string; $this->data_string_length = strlen($string); } + /** + * @phpstan-impure + * + * @return int|bool + */ protected function ftell() { if ($this->data_string_flag) { return $this->data_string_position; @@ -1702,11 +2194,25 @@ abstract class getid3_handler { return ftell($this->getid3->fp); } + /** + * @param int $bytes + * + * @phpstan-impure + * + * @return string|false + * + * @throws getid3_exception + */ protected function fread($bytes) { if ($this->data_string_flag) { $this->data_string_position += $bytes; return substr($this->data_string, $this->data_string_position - $bytes, $bytes); } + if ($bytes == 0) { + return ''; + } elseif ($bytes < 0) { + throw new getid3_exception('cannot fread('.$bytes.' from '.$this->ftell().')', 10); + } $pos = $this->ftell() + $bytes; if (!getid3_lib::intValueSupported($pos)) { throw new getid3_exception('cannot fread('.$bytes.' from '.$this->ftell().') because beyond PHP filesystem limit', 10); @@ -1714,7 +2220,7 @@ abstract class getid3_handler { //return fread($this->getid3->fp, $bytes); /* - * http://www.getid3.org/phpBB3/viewtopic.php?t=1930 + * https://www.getid3.org/phpBB3/viewtopic.php?t=1930 * "I found out that the root cause for the problem was how getID3 uses the PHP system function fread(). * It seems to assume that fread() would always return as many bytes as were requested. * However, according the PHP manual (http://php.net/manual/en/function.fread.php), this is the case only with regular local files, but not e.g. with Linux pipes. @@ -1722,6 +2228,10 @@ abstract class getid3_handler { */ $contents = ''; do { + //if (($this->getid3->memory_limit > 0) && ($bytes > $this->getid3->memory_limit)) { + if (($this->getid3->memory_limit > 0) && (($bytes / $this->getid3->memory_limit) > 0.99)) { // enable a more-fuzzy match to prevent close misses generating errors like "PHP Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 33554464 bytes)" + throw new getid3_exception('cannot fread('.$bytes.' from '.$this->ftell().') that is more than available PHP memory ('.$this->getid3->memory_limit.')', 10); + } $part = fread($this->getid3->fp, $bytes); $partLength = strlen($part); $bytes -= $partLength; @@ -1730,6 +2240,16 @@ abstract class getid3_handler { return $contents; } + /** + * @param int $bytes + * @param int $whence + * + * @phpstan-impure + * + * @return int + * + * @throws getid3_exception + */ protected function fseek($bytes, $whence=SEEK_SET) { if ($this->data_string_flag) { switch ($whence) { @@ -1745,21 +2265,89 @@ abstract class getid3_handler { $this->data_string_position = $this->data_string_length + $bytes; break; } - return 0; - } else { - $pos = $bytes; - if ($whence == SEEK_CUR) { - $pos = $this->ftell() + $bytes; - } elseif ($whence == SEEK_END) { - $pos = $this->getid3->info['filesize'] + $bytes; - } - if (!getid3_lib::intValueSupported($pos)) { - throw new getid3_exception('cannot fseek('.$pos.') because beyond PHP filesystem limit', 10); - } + return 0; // fseek returns 0 on success } - return fseek($this->getid3->fp, $bytes, $whence); + + $pos = $bytes; + if ($whence == SEEK_CUR) { + $pos = $this->ftell() + $bytes; + } elseif ($whence == SEEK_END) { + $pos = $this->getid3->info['filesize'] + $bytes; + } + if (!getid3_lib::intValueSupported($pos)) { + throw new getid3_exception('cannot fseek('.$pos.') because beyond PHP filesystem limit', 10); + } + + // https://github.com/JamesHeinrich/getID3/issues/327 + $result = fseek($this->getid3->fp, $bytes, $whence); + if ($result !== 0) { // fseek returns 0 on success + throw new getid3_exception('cannot fseek('.$pos.'). resource/stream does not appear to support seeking', 10); + } + return $result; } + /** + * @phpstan-impure + * + * @return string|false + * + * @throws getid3_exception + */ + protected function fgets() { + // must be able to handle CR/LF/CRLF but not read more than one lineend + $buffer = ''; // final string we will return + $prevchar = ''; // save previously-read character for end-of-line checking + if ($this->data_string_flag) { + while (true) { + $thischar = substr($this->data_string, $this->data_string_position++, 1); + if (($prevchar == "\r") && ($thischar != "\n")) { + // read one byte too many, back up + $this->data_string_position--; + break; + } + $buffer .= $thischar; + if ($thischar == "\n") { + break; + } + if ($this->data_string_position >= $this->data_string_length) { + // EOF + break; + } + $prevchar = $thischar; + } + + } else { + + // Ideally we would just use PHP's fgets() function, however... + // it does not behave consistently with regards to mixed line endings, may be system-dependent + // and breaks entirely when given a file with mixed \r vs \n vs \r\n line endings (e.g. some PDFs) + //return fgets($this->getid3->fp); + while (true) { + $thischar = fgetc($this->getid3->fp); + if (($prevchar == "\r") && ($thischar != "\n")) { + // read one byte too many, back up + fseek($this->getid3->fp, -1, SEEK_CUR); + break; + } + $buffer .= $thischar; + if ($thischar == "\n") { + break; + } + if (feof($this->getid3->fp)) { + break; + } + $prevchar = $thischar; + } + + } + return $buffer; + } + + /** + * @phpstan-impure + * + * @return bool + */ protected function feof() { if ($this->data_string_flag) { return $this->data_string_position >= $this->data_string_length; @@ -1767,25 +2355,56 @@ abstract class getid3_handler { return feof($this->getid3->fp); } + /** + * @param string $module + * + * @return bool + */ final protected function isDependencyFor($module) { return $this->dependency_to == $module; } + /** + * @param string $text + * + * @return bool + */ protected function error($text) { $this->getid3->info['error'][] = $text; return false; } + /** + * @param string $text + * + * @return bool + */ protected function warning($text) { return $this->getid3->warning($text); } + /** + * @param string $text + */ protected function notice($text) { // does nothing for now } + /** + * @param string $name + * @param int $offset + * @param int $length + * @param string $image_mime + * + * @return string|null + * + * @throws Exception + * @throws getid3_exception + */ public function saveAttachment($name, $offset, $length, $image_mime=null) { + $fp_dest = null; + $dest = null; try { // do not extract at all @@ -1838,6 +2457,9 @@ abstract class getid3_handler { // close and remove dest file if created if (isset($fp_dest) && is_resource($fp_dest)) { fclose($fp_dest); + } + + if (isset($dest) && file_exists($dest)) { unlink($dest); } diff --git a/lib/getid3/module.archive.gzip.php b/lib/getid3/module.archive.gzip.php index 112a1e6..d757781 100644 --- a/lib/getid3/module.archive.gzip.php +++ b/lib/getid3/module.archive.gzip.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.gzip.php // @@ -19,12 +19,23 @@ // // ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} -class getid3_gzip extends getid3_handler { - - // public: Optional file list - disable for speed. - public $option_gzip_parse_contents = false; // decode gzipped files, if possible, and parse recursively (.tar.gz for example) +class getid3_gzip extends getid3_handler +{ + /** + * Optional file list - disable for speed. + * Decode gzipped files, if possible, and parse recursively (.tar.gz for example). + * + * @var bool + */ + public $parse_contents = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -44,9 +55,10 @@ class getid3_gzip extends getid3_handler { $buffer = $this->fread($info['filesize']); $arr_members = explode("\x1F\x8B\x08", $buffer); + $num_members = 0; while (true) { $is_wrong_members = false; - $num_members = intval(count($arr_members)); + $num_members = count($arr_members); for ($i = 0; $i < $num_members; $i++) { if (strlen($arr_members[$i]) == 0) { continue; @@ -71,13 +83,13 @@ class getid3_gzip extends getid3_handler { $fpointer = 0; $idx = 0; - for ($i = 0; $i < $num_members; $i++) { - if (strlen($arr_members[$i]) == 0) { + foreach ($arr_members as $member) { + if (strlen($member) == 0) { continue; } $thisInfo = &$info['gzip']['member_header'][++$idx]; - $buff = "\x1F\x8B\x08".$arr_members[$i]; + $buff = "\x1F\x8B\x08". $member; $attr = unpack($unpack_header, substr($buff, 0, $start_length)); $thisInfo['filemtime'] = getid3_lib::LittleEndian2Int($attr['mtime']); @@ -187,7 +199,7 @@ class getid3_gzip extends getid3_handler { $info['gzip']['files'] = getid3_lib::array_merge_clobber($info['gzip']['files'], getid3_lib::CreateDeepArray($thisInfo['filename'], '/', $thisInfo['filesize'])); - if ($this->option_gzip_parse_contents) { + if ($this->parse_contents) { // Try to inflate GZip $csize = 0; $inflated = ''; @@ -199,7 +211,7 @@ class getid3_gzip extends getid3_handler { $inflated = gzinflate($cdata); // Calculate CRC32 for inflated content - $thisInfo['crc32_valid'] = (bool) (sprintf('%u', crc32($inflated)) == $thisInfo['crc32']); + $thisInfo['crc32_valid'] = sprintf('%u', crc32($inflated)) == $thisInfo['crc32']; // determine format $formattest = substr($inflated, 0, 32774); @@ -240,13 +252,21 @@ class getid3_gzip extends getid3_handler { // unknown or unhandled format break; } + } else { + $this->warning('PHP is not compiled with gzinflate() support. Please enable PHP Zlib extension or recompile with the --with-zlib switch'); } } } return true; } - // Converts the OS type + /** + * Converts the OS type. + * + * @param string $key + * + * @return string + */ public function get_os_type($key) { static $os_type = array( '0' => 'FAT filesystem (MS-DOS, OS/2, NT/Win32)', @@ -268,7 +288,13 @@ class getid3_gzip extends getid3_handler { return (isset($os_type[$key]) ? $os_type[$key] : ''); } - // Converts the eXtra FLags + /** + * Converts the eXtra FLags. + * + * @param string $key + * + * @return string + */ public function get_xflag_type($key) { static $xflag_type = array( '0' => 'unknown', diff --git a/lib/getid3/module.archive.hpk.php b/lib/getid3/module.archive.hpk.php new file mode 100644 index 0000000..10c20dc --- /dev/null +++ b/lib/getid3/module.archive.hpk.php @@ -0,0 +1,92 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.archive.hpk.php // +// module for analyzing HPK files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_hpk extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $info['fileformat'] = 'hpk'; + + $this->fseek($info['avdataoffset']); + $HPKheader = $this->fread(36); + + if (substr($HPKheader, 0, 4) == 'BPUL') { + + $info['hpk']['header']['signature'] = substr($HPKheader, 0, 4); + $info['hpk']['header']['data_offset'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 4, 4)); + $info['hpk']['header']['fragments_per_file'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 8, 4)); + //$info['hpk']['header']['unknown1'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 12, 4)); + $info['hpk']['header']['fragments_residual_offset'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 16, 4)); + $info['hpk']['header']['fragments_residual_count'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 20, 4)); + //$info['hpk']['header']['unknown2'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 24, 4)); + $info['hpk']['header']['fragmented_filesystem_offset'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 28, 4)); + $info['hpk']['header']['fragmented_filesystem_length'] = getid3_lib::LittleEndian2Int(substr($HPKheader, 32, 4)); + + $info['hpk']['header']['filesystem_entries'] = getid3_lib::SafeDiv($info['hpk']['header']['fragmented_filesystem_length'], $info['hpk']['header']['fragments_per_file'] * 8); + $this->fseek($info['hpk']['header']['fragmented_filesystem_offset']); + for ($i = 0; $i < $info['hpk']['header']['filesystem_entries']; $i++) { + $offset = getid3_lib::LittleEndian2Int($this->fread(4)); + $length = getid3_lib::LittleEndian2Int($this->fread(4)); + $info['hpk']['filesystem'][$i] = array('offset' => $offset, 'length' => $length); + } + +$this->error('HPK parsing incomplete (and mostly broken) in this version of getID3() ['.$this->getid3->version().']'); + +/* + $filename = ''; + $dirs = array(); + foreach ($info['hpk']['filesystem'] as $key => $filesystemdata) { + $this->fseek($filesystemdata['offset']); + $first4 = $this->fread(4); + if (($first4 == 'LZ4 ') || ($first4 == 'ZLIB')) { + // actual data, ignore + $info['hpk']['toc'][$key] = array( + 'filename' => ltrim(implode('/', $dirs).'/'.$filename, '/'), + 'offset' => $filesystemdata['offset'], + 'length' => $filesystemdata['length'], + ); + $filename = ''; + $dirs = array(); + } else { + $fragment_index = getid3_lib::LittleEndian2Int($first4); + $fragment_type = getid3_lib::LittleEndian2Int($this->fread(4)); // file = 0, directory = 1 + $name_length = getid3_lib::LittleEndian2Int($this->fread(2)); + if ($fragment_type == 1) { + $dirs[] = $this->fread($name_length); + } else { + $filename = $this->fread($name_length); + } + } + } +*/ + + } else { + $this->error('Expecting "BPUL" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($HPKheader, 0, 4)).'"'); + return false; + } + + return true; + } + +} diff --git a/lib/getid3/module.archive.rar.php b/lib/getid3/module.archive.rar.php index 30961fb..f80219c 100644 --- a/lib/getid3/module.archive.rar.php +++ b/lib/getid3/module.archive.rar.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.rar.php // @@ -14,18 +14,28 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_rar extends getid3_handler { + /** + * if true use PHP RarArchive extension, if false (non-extension parsing not yet written in getID3) + * + * @var bool + */ + public $use_php_rar_extension = true; - public $option_use_rar_extension = false; - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $info['fileformat'] = 'rar'; - if ($this->option_use_rar_extension === true) { + if ($this->use_php_rar_extension === true) { if (function_exists('rar_open')) { if ($rp = rar_open($info['filenamepath'])) { $info['rar']['files'] = array(); @@ -42,7 +52,7 @@ class getid3_rar extends getid3_handler $this->error('RAR support does not appear to be available in this PHP installation'); } } else { - $this->error('PHP-RAR processing has been disabled (set $getid3_rar->option_use_rar_extension=true to enable)'); + $this->error('PHP-RAR processing has been disabled (set $getid3_rar->use_php_rar_extension=true to enable)'); } return false; diff --git a/lib/getid3/module.archive.szip.php b/lib/getid3/module.archive.szip.php index 6751729..8647967 100644 --- a/lib/getid3/module.archive.szip.php +++ b/lib/getid3/module.archive.szip.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.szip.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_szip extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -30,8 +35,8 @@ class getid3_szip extends getid3_handler $info['fileformat'] = 'szip'; $info['szip']['major_version'] = getid3_lib::BigEndian2Int(substr($SZIPHeader, 4, 1)); $info['szip']['minor_version'] = getid3_lib::BigEndian2Int(substr($SZIPHeader, 5, 1)); -$this->error('SZIP parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; + $this->error('SZIP parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; while (!$this->feof()) { $NextBlockID = $this->fread(2); diff --git a/lib/getid3/module.archive.tar.php b/lib/getid3/module.archive.tar.php index 9e8d72a..f8aed79 100644 --- a/lib/getid3/module.archive.tar.php +++ b/lib/getid3/module.archive.tar.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.tar.php // @@ -19,10 +19,15 @@ // // ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_tar extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -42,13 +47,13 @@ class getid3_tar extends getid3_handler // check the block $checksum = 0; for ($i = 0; $i < 148; $i++) { - $checksum += ord($buffer{$i}); + $checksum += ord($buffer[$i]); } for ($i = 148; $i < 156; $i++) { $checksum += ord(' '); } for ($i = 156; $i < 512; $i++) { - $checksum += ord($buffer{$i}); + $checksum += ord($buffer[$i]); } $attr = unpack($unpack_header, $buffer); $name = (isset($attr['fname'] ) ? trim($attr['fname'] ) : ''); @@ -117,7 +122,13 @@ class getid3_tar extends getid3_handler return true; } - // Parses the file mode to file permissions + /** + * Parses the file mode to file permissions. + * + * @param int $mode + * + * @return string + */ public function display_perms($mode) { // Determine Type if ($mode & 0x1000) $type='p'; // FIFO pipe @@ -130,6 +141,9 @@ class getid3_tar extends getid3_handler else $type='u'; // UNKNOWN // Determine permissions + $owner = array(); + $group = array(); + $world = array(); $owner['read'] = (($mode & 00400) ? 'r' : '-'); $owner['write'] = (($mode & 00200) ? 'w' : '-'); $owner['execute'] = (($mode & 00100) ? 'x' : '-'); @@ -152,7 +166,13 @@ class getid3_tar extends getid3_handler return $s; } - // Converts the file type + /** + * Converts the file type. + * + * @param string $typflag + * + * @return mixed|string + */ public function get_flag_type($typflag) { static $flag_types = array( '0' => 'LF_NORMAL', diff --git a/lib/getid3/module.archive.xz.php b/lib/getid3/module.archive.xz.php new file mode 100644 index 0000000..dbbc067 --- /dev/null +++ b/lib/getid3/module.archive.xz.php @@ -0,0 +1,44 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.archive.xz.php // +// module for analyzing XZ files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_xz extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $this->fseek($info['avdataoffset']); + $xzheader = $this->fread(6); + + // https://tukaani.org/xz/xz-file-format-1.0.4.txt + $info['xz']['stream_header']['magic'] = substr($xzheader, 0, 6); + if ($info['xz']['stream_header']['magic'] != "\xFD".'7zXZ'."\x00") { + $this->error('Invalid XZ stream header magic (expecting FD 37 7A 58 5A 00, found '.getid3_lib::PrintHexBytes($info['xz']['stream_header']['magic']).') at offset '.$info['avdataoffset']); + return false; + } + $info['fileformat'] = 'xz'; + $this->error('XZ parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; + + } + +} diff --git a/lib/getid3/module.archive.zip.php b/lib/getid3/module.archive.zip.php index 4b7aa58..ffd196d 100644 --- a/lib/getid3/module.archive.zip.php +++ b/lib/getid3/module.archive.zip.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.zip.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_zip extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -48,7 +53,7 @@ class getid3_zip extends getid3_handler $this->fseek($info['zip']['end_central_directory']['directory_offset']); $info['zip']['entries_count'] = 0; - while ($centraldirectoryentry = $this->ZIPparseCentralDirectory($this->getid3->fp)) { + while ($centraldirectoryentry = $this->ZIPparseCentralDirectory()) { $info['zip']['central_directory'][] = $centraldirectoryentry; $info['zip']['entries_count']++; $info['zip']['compressed_size'] += $centraldirectoryentry['compressed_size']; @@ -90,19 +95,39 @@ class getid3_zip extends getid3_handler } } + // check for EPUB files + if (!empty($info['zip']['entries'][0]['filename']) && + ($info['zip']['entries'][0]['filename'] == 'mimetype') && + ($info['zip']['entries'][0]['compression_method'] == 'store') && + ($info['zip']['entries'][0]['uncompressed_size'] == 20) && + isset($info['zip']['entries'][0]['data_offset'])) { + // http://idpf.org/epub/30/spec/epub30-ocf.html + // "3.3 OCF ZIP Container Media Type Identification + // OCF ZIP Containers must include a mimetype file as the first file in the Container, and the contents of this file must be the MIME type string application/epub+zip. + // The contents of the mimetype file must not contain any leading padding or whitespace, must not begin with the Unicode signature (or Byte Order Mark), + // and the case of the MIME type string must be exactly as presented above. The mimetype file additionally must be neither compressed nor encrypted, + // and there must not be an extra field in its ZIP header." + $this->fseek($info['zip']['entries'][0]['data_offset']); + if ($this->fread(20) == 'application/epub+zip') { + $info['fileformat'] = 'zip.epub'; + $info['mime_type'] = 'application/epub+zip'; + } + } + + // check for Office Open XML files (e.g. .docx, .xlsx) if (!empty($info['zip']['files']['[Content_Types].xml']) && !empty($info['zip']['files']['_rels']['.rels']) && !empty($info['zip']['files']['docProps']['app.xml']) && !empty($info['zip']['files']['docProps']['core.xml'])) { - // http://technet.microsoft.com/en-us/library/cc179224.aspx - $info['fileformat'] = 'zip.msoffice'; - if (!empty($ThisFileInfo['zip']['files']['ppt'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; - } elseif (!empty($ThisFileInfo['zip']['files']['xl'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - } elseif (!empty($ThisFileInfo['zip']['files']['word'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; - } + // http://technet.microsoft.com/en-us/library/cc179224.aspx + $info['fileformat'] = 'zip.msoffice'; + if (!empty($info['zip']['files']['ppt'])) { + $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; + } elseif (!empty($info['zip']['files']['xl'])) { + $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } elseif (!empty($info['zip']['files']['word'])) { + $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + } } return true; @@ -129,7 +154,9 @@ class getid3_zip extends getid3_handler return true; } - + /** + * @return bool + */ public function getZIPHeaderFilepointerTopDown() { $info = &$this->getid3->info; @@ -150,7 +177,7 @@ class getid3_zip extends getid3_handler } $info['zip']['entries_count'] = 0; - while ($centraldirectoryentry = $this->ZIPparseCentralDirectory($this->getid3->fp)) { + while ($centraldirectoryentry = $this->ZIPparseCentralDirectory()) { $info['zip']['central_directory'][] = $centraldirectoryentry; $info['zip']['entries_count']++; $info['zip']['compressed_size'] += $centraldirectoryentry['compressed_size']; @@ -175,7 +202,9 @@ class getid3_zip extends getid3_handler return true; } - + /** + * @return bool + */ public function getZIPentriesFilepointer() { $info = &$this->getid3->info; @@ -198,8 +227,11 @@ class getid3_zip extends getid3_handler return true; } - + /** + * @return array|false + */ public function ZIPparseLocalFileHeader() { + $LocalFileHeader = array(); $LocalFileHeader['offset'] = $this->ftell(); $ZIPlocalFileHeader = $this->fread(30); @@ -265,7 +297,7 @@ class getid3_zip extends getid3_handler $DataDescriptor = $this->fread(16); $LocalFileHeader['data_descriptor']['signature'] = getid3_lib::LittleEndian2Int(substr($DataDescriptor, 0, 4)); if ($LocalFileHeader['data_descriptor']['signature'] != 0x08074B50) { // "PK\x07\x08" - $this->getid3->warning[] = 'invalid Local File Header Data Descriptor Signature at offset '.($this->ftell() - 16).' - expecting 08 07 4B 50, found '.getid3_lib::PrintHexBytes($LocalFileHeader['data_descriptor']['signature']); + $this->getid3->warning('invalid Local File Header Data Descriptor Signature at offset '.($this->ftell() - 16).' - expecting 08 07 4B 50, found '.getid3_lib::PrintHexBytes(substr($DataDescriptor, 0, 4))); $this->fseek($LocalFileHeader['offset']); // seek back to where filepointer originally was so it can be handled properly return false; } @@ -294,8 +326,11 @@ class getid3_zip extends getid3_handler return $LocalFileHeader; } - + /** + * @return array|false + */ public function ZIPparseCentralDirectory() { + $CentralDirectory = array(); $CentralDirectory['offset'] = $this->ftell(); $ZIPcentralDirectory = $this->fread(46); @@ -351,7 +386,11 @@ class getid3_zip extends getid3_handler return $CentralDirectory; } + /** + * @return array|false + */ public function ZIPparseEndOfCentralDirectory() { + $EndOfCentralDirectory = array(); $EndOfCentralDirectory['offset'] = $this->ftell(); $ZIPendOfCentralDirectory = $this->fread(22); @@ -377,9 +416,15 @@ class getid3_zip extends getid3_handler return $EndOfCentralDirectory; } - + /** + * @param int $flagbytes + * @param int $compressionmethod + * + * @return array + */ public static function ZIPparseGeneralPurposeFlags($flagbytes, $compressionmethod) { // https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip-printable.html + $ParsedFlags = array(); $ParsedFlags['encrypted'] = (bool) ($flagbytes & 0x0001); // 0x0002 -- see below // 0x0004 -- see below @@ -425,7 +470,11 @@ class getid3_zip extends getid3_handler return $ParsedFlags; } - + /** + * @param int $index + * + * @return string + */ public static function ZIPversionOSLookup($index) { static $ZIPversionOSLookup = array( 0 => 'MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)', @@ -453,6 +502,11 @@ class getid3_zip extends getid3_handler return (isset($ZIPversionOSLookup[$index]) ? $ZIPversionOSLookup[$index] : '[unknown]'); } + /** + * @param int $index + * + * @return string + */ public static function ZIPcompressionMethodLookup($index) { // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ZIP.html static $ZIPcompressionMethodLookup = array( @@ -484,6 +538,12 @@ class getid3_zip extends getid3_handler return (isset($ZIPcompressionMethodLookup[$index]) ? $ZIPcompressionMethodLookup[$index] : '[unknown]'); } + /** + * @param int $DOSdate + * @param int $DOStime + * + * @return int + */ public static function DOStime2UNIXtime($DOSdate, $DOStime) { // wFatDate // Specifies the MS-DOS date. The date is a packed 16-bit value with the following format: diff --git a/lib/getid3/module.audio-video.asf.php b/lib/getid3/module.audio-video.asf.php index 23d3a0e..4d3d377 100644 --- a/lib/getid3/module.audio-video.asf.php +++ b/lib/getid3/module.audio-video.asf.php @@ -1,11 +1,10 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.asf.php // @@ -14,10 +13,34 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); -class getid3_asf extends getid3_handler { +class getid3_asf extends getid3_handler +{ + protected static $ASFIndexParametersObjectIndexSpecifiersIndexTypes = array( + 1 => 'Nearest Past Data Packet', + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint' + ); + protected static $ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes = array( + 1 => 'Nearest Past Data Packet', + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint', + 0xFF => 'Frame Number Offset' + ); + + protected static $ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes = array( + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint' + ); + + /** + * @param getID3 $getid3 + */ public function __construct(getID3 $getid3) { parent::__construct($getid3); // extends getid3_handler::__construct() @@ -30,6 +53,9 @@ class getid3_asf extends getid3_handler { } } + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -83,6 +109,9 @@ class getid3_asf extends getid3_handler { $NextObjectOffset = $this->ftell(); $ASFHeaderData = $this->fread($thisfile_asf_headerobject['objectsize'] - 30); $offset = 0; + $thisfile_asf_streambitratepropertiesobject = array(); + $thisfile_asf_codeclistobject = array(); + $StreamPropertiesObjectData = array(); for ($HeaderObjectsCounter = 0; $HeaderObjectsCounter < $thisfile_asf_headerobject['headerobjects']; $HeaderObjectsCounter++) { $NextObjectGUID = substr($ASFHeaderData, $offset, 16); @@ -164,7 +193,7 @@ class getid3_asf extends getid3_handler { $info['playtime_seconds'] = ($thisfile_asf_filepropertiesobject['play_duration'] / 10000000) - ($thisfile_asf_filepropertiesobject['preroll'] / 1000); //$info['bitrate'] = $thisfile_asf_filepropertiesobject['max_bitrate']; - $info['bitrate'] = ((isset($thisfile_asf_filepropertiesobject['filesize']) ? $thisfile_asf_filepropertiesobject['filesize'] : $info['filesize']) * 8) / $info['playtime_seconds']; + $info['bitrate'] = getid3_lib::SafeDiv($thisfile_asf_filepropertiesobject['filesize'] * 8, $info['playtime_seconds']); } break; @@ -273,7 +302,7 @@ class getid3_asf extends getid3_handler { $thisfile_asf_headerextensionobject['reserved_2'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); $offset += 2; if ($thisfile_asf_headerextensionobject['reserved_2'] != 6) { - $this->warning('header_extension_object.reserved_2 ('.getid3_lib::PrintHexBytes($thisfile_asf_headerextensionobject['reserved_2']).') does not match expected value of "6"'); + $this->warning('header_extension_object.reserved_2 ('.$thisfile_asf_headerextensionobject['reserved_2'].') does not match expected value of "6"'); //return false; break; } @@ -306,6 +335,7 @@ class getid3_asf extends getid3_handler { // shortcut $thisfile_asf['codec_list_object'] = array(); + /** @var mixed[] $thisfile_asf_codeclistobject */ $thisfile_asf_codeclistobject = &$thisfile_asf['codec_list_object']; $thisfile_asf_codeclistobject['offset'] = $NextObjectOffset + $offset; @@ -321,6 +351,9 @@ class getid3_asf extends getid3_handler { break; } $thisfile_asf_codeclistobject['codec_entries_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); + if ($thisfile_asf_codeclistobject['codec_entries_count'] > 0) { + $thisfile_asf_codeclistobject['codec_entries'] = array(); + } $offset += 4; for ($CodecEntryCounter = 0; $CodecEntryCounter < $thisfile_asf_codeclistobject['codec_entries_count']; $CodecEntryCounter++) { // shortcut @@ -356,7 +389,7 @@ class getid3_asf extends getid3_handler { $thisfile_audio['codec'] = $this->TrimConvert($thisfile_asf_codeclistobject_codecentries_current['name']); if (!isset($thisfile_audio['bitrate']) && strstr($AudioCodecBitrate, 'kbps')) { - $thisfile_audio['bitrate'] = (int) (trim(str_replace('kbps', '', $AudioCodecBitrate)) * 1000); + $thisfile_audio['bitrate'] = (int) trim(str_replace('kbps', '', $AudioCodecBitrate)) * 1000; } //if (!isset($thisfile_video['bitrate']) && isset($thisfile_audio['bitrate']) && isset($thisfile_asf['file_properties_object']['max_bitrate']) && ($thisfile_asf_codeclistobject['codec_entries_count'] > 1)) { if (empty($thisfile_video['bitrate']) && !empty($thisfile_audio['bitrate']) && !empty($info['bitrate'])) { @@ -517,7 +550,7 @@ class getid3_asf extends getid3_handler { $offset += 16; $thisfile_asf_markerobject['reserved_guid'] = $this->BytestringToGUID($thisfile_asf_markerobject['reserved']); if ($thisfile_asf_markerobject['reserved'] != $this->GUIDtoBytestring('4CFEDB20-75F6-11CF-9C0F-00A0C90349CB')) { - $this->warning('marker_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_markerobject['reserved_1']).'} does not match expected "GETID3_ASF_Reserved_1" GUID {4CFEDB20-75F6-11CF-9C0F-00A0C90349CB}'); + $this->warning('marker_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_markerobject['reserved']).'} does not match expected "GETID3_ASF_Reserved_1" GUID {4CFEDB20-75F6-11CF-9C0F-00A0C90349CB}'); break; } $thisfile_asf_markerobject['markers_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); @@ -525,7 +558,7 @@ class getid3_asf extends getid3_handler { $thisfile_asf_markerobject['reserved_2'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); $offset += 2; if ($thisfile_asf_markerobject['reserved_2'] != 0) { - $this->warning('marker_object.reserved_2 ('.getid3_lib::PrintHexBytes($thisfile_asf_markerobject['reserved_2']).') does not match expected value of "0"'); + $this->warning('marker_object.reserved_2 ('.$thisfile_asf_markerobject['reserved_2'].') does not match expected value of "0"'); break; } $thisfile_asf_markerobject['name_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); @@ -637,7 +670,7 @@ class getid3_asf extends getid3_handler { break; default: - $this->warning('error_correction_object.error_correction_type GUID {'.$this->BytestringToGUID($thisfile_asf_errorcorrectionobject['reserved']).'} does not match expected "GETID3_ASF_No_Error_Correction" GUID {'.$this->BytestringToGUID(GETID3_ASF_No_Error_Correction).'} or "GETID3_ASF_Audio_Spread" GUID {'.$this->BytestringToGUID(GETID3_ASF_Audio_Spread).'}'); + $this->warning('error_correction_object.error_correction_type GUID {'.$this->BytestringToGUID($thisfile_asf_errorcorrectionobject['error_correction_type']).'} does not match expected "GETID3_ASF_No_Error_Correction" GUID {'.$this->BytestringToGUID(GETID3_ASF_No_Error_Correction).'} or "GETID3_ASF_Audio_Spread" GUID {'.$this->BytestringToGUID(GETID3_ASF_Audio_Spread).'}'); //return false; break; } @@ -790,17 +823,17 @@ class getid3_asf extends getid3_handler { case 'wm/tracknumber': case 'tracknumber': // be careful casting to int: casting unicode strings to int gives unexpected results (stops parsing at first non-numeric character) - $thisfile_asf_comments['track'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - foreach ($thisfile_asf_comments['track'] as $key => $value) { + $thisfile_asf_comments['track_number'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); + foreach ($thisfile_asf_comments['track_number'] as $key => $value) { if (preg_match('/^[0-9\x00]+$/', $value)) { - $thisfile_asf_comments['track'][$key] = intval(str_replace("\x00", '', $value)); + $thisfile_asf_comments['track_number'][$key] = intval(str_replace("\x00", '', $value)); } } break; case 'wm/track': - if (empty($thisfile_asf_comments['track'])) { - $thisfile_asf_comments['track'] = array(1 + $this->TrimConvert($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); + if (empty($thisfile_asf_comments['track_number'])) { + $thisfile_asf_comments['track_number'] = array(1 + (int) $this->TrimConvert($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); } break; @@ -970,18 +1003,18 @@ class getid3_asf extends getid3_handler { break; } } - if (isset($thisfile_asf_streambitrateproperties['bitrate_records_count'])) { + if (isset($thisfile_asf_streambitratepropertiesobject['bitrate_records_count'])) { $ASFbitrateAudio = 0; $ASFbitrateVideo = 0; - for ($BitrateRecordsCounter = 0; $BitrateRecordsCounter < $thisfile_asf_streambitrateproperties['bitrate_records_count']; $BitrateRecordsCounter++) { + for ($BitrateRecordsCounter = 0; $BitrateRecordsCounter < $thisfile_asf_streambitratepropertiesobject['bitrate_records_count']; $BitrateRecordsCounter++) { if (isset($thisfile_asf_codeclistobject['codec_entries'][$BitrateRecordsCounter])) { switch ($thisfile_asf_codeclistobject['codec_entries'][$BitrateRecordsCounter]['type_raw']) { case 1: - $ASFbitrateVideo += $thisfile_asf_streambitrateproperties['bitrate_records'][$BitrateRecordsCounter]['bitrate']; + $ASFbitrateVideo += $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['bitrate']; break; case 2: - $ASFbitrateAudio += $thisfile_asf_streambitrateproperties['bitrate_records'][$BitrateRecordsCounter]['bitrate']; + $ASFbitrateAudio += $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['bitrate']; break; default: @@ -1033,7 +1066,7 @@ class getid3_asf extends getid3_handler { break; } - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { + if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { // @phpstan-ignore-line foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { $thisfile_asf_audiomedia_currentstream['bitrate'] = $dataarray['bitrate']; @@ -1119,7 +1152,7 @@ class getid3_asf extends getid3_handler { $videomediaoffset += 4; $thisfile_asf_videomedia_currentstream['format_data']['codec_data'] = substr($streamdata['type_specific_data'], $videomediaoffset); - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { + if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { // @phpstan-ignore-line foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { $thisfile_asf_videomedia_currentstream['bitrate'] = $dataarray['bitrate']; @@ -1183,7 +1216,7 @@ class getid3_asf extends getid3_handler { $thisfile_asf_dataobject['reserved'] = getid3_lib::LittleEndian2Int(substr($DataObjectData, $offset, 2)); $offset += 2; if ($thisfile_asf_dataobject['reserved'] != 0x0101) { - $this->warning('data_object.reserved ('.getid3_lib::PrintHexBytes($thisfile_asf_dataobject['reserved']).') does not match expected value of "0x0101"'); + $this->warning('data_object.reserved (0x'.sprintf('%04X', $thisfile_asf_dataobject['reserved']).') does not match expected value of "0x0101"'); //return false; break; } @@ -1431,7 +1464,7 @@ class getid3_asf extends getid3_handler { } } } - $info['bitrate'] = (isset($thisfile_audio['bitrate']) ? $thisfile_audio['bitrate'] : 0) + (isset($thisfile_video['bitrate']) ? $thisfile_video['bitrate'] : 0); + $info['bitrate'] = 0 + (isset($thisfile_audio['bitrate']) ? $thisfile_audio['bitrate'] : 0) + (isset($thisfile_video['bitrate']) ? $thisfile_video['bitrate'] : 0); if ((!isset($info['playtime_seconds']) || ($info['playtime_seconds'] <= 0)) && ($info['bitrate'] > 0)) { $info['playtime_seconds'] = ($info['filesize'] - $info['avdataoffset']) / ($info['bitrate'] / 8); @@ -1440,6 +1473,11 @@ class getid3_asf extends getid3_handler { return true; } + /** + * @param int $CodecListType + * + * @return string + */ public static function codecListObjectTypeLookup($CodecListType) { static $lookup = array( 0x0001 => 'Video Codec', @@ -1450,6 +1488,9 @@ class getid3_asf extends getid3_handler { return (isset($lookup[$CodecListType]) ? $lookup[$CodecListType] : 'Invalid Codec Type'); } + /** + * @return array + */ public static function KnownGUIDs() { static $GUIDarray = array( 'GETID3_ASF_Extended_Stream_Properties_Object' => '14E6A5CB-C672-4332-8399-A96952065B5A', @@ -1558,12 +1599,18 @@ class getid3_asf extends getid3_handler { 'GETID3_ASF_Audio_Media' => 'F8699E40-5B4D-11CF-A8FD-00805F5C442B', 'GETID3_ASF_Media_Object_Index_Object' => 'FEB103F8-12AD-4C64-840F-2A1D2F7AD48C', 'GETID3_ASF_Alt_Extended_Content_Encryption_Obj' => 'FF889EF1-ADEE-40DA-9E71-98704BB928CE', - 'GETID3_ASF_Index_Placeholder_Object' => 'D9AADE20-7C17-4F9C-BC28-8555DD98E2A2', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html - 'GETID3_ASF_Compatibility_Object' => '26F18B5D-4584-47EC-9F5F-0E651F0452C9', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html + 'GETID3_ASF_Index_Placeholder_Object' => 'D9AADE20-7C17-4F9C-BC28-8555DD98E2A2', // https://metacpan.org/dist/Audio-WMA/source/WMA.pm + 'GETID3_ASF_Compatibility_Object' => '26F18B5D-4584-47EC-9F5F-0E651F0452C9', // https://metacpan.org/dist/Audio-WMA/source/WMA.pm + 'GETID3_ASF_Media_Object_Index_Parameters_Object'=> '6B203BAD-3F11-48E4-ACA8-D7613DE2CFA7', ); return $GUIDarray; } + /** + * @param string $GUIDstring + * + * @return string|false + */ public static function GUIDname($GUIDstring) { static $GUIDarray = array(); if (empty($GUIDarray)) { @@ -1572,6 +1619,11 @@ class getid3_asf extends getid3_handler { return array_search($GUIDstring, $GUIDarray); } + /** + * @param int $id + * + * @return string + */ public static function ASFIndexObjectIndexTypeLookup($id) { static $ASFIndexObjectIndexTypeLookup = array(); if (empty($ASFIndexObjectIndexTypeLookup)) { @@ -1582,6 +1634,11 @@ class getid3_asf extends getid3_handler { return (isset($ASFIndexObjectIndexTypeLookup[$id]) ? $ASFIndexObjectIndexTypeLookup[$id] : 'invalid'); } + /** + * @param string $GUIDstring + * + * @return string + */ public static function GUIDtoBytestring($GUIDstring) { // Microsoft defines these 16-byte (128-bit) GUIDs in the strangest way: // first 4 bytes are in little-endian order @@ -1617,31 +1674,42 @@ class getid3_asf extends getid3_handler { return $hexbytecharstring; } + /** + * @param string $Bytestring + * + * @return string + */ public static function BytestringToGUID($Bytestring) { - $GUIDstring = str_pad(dechex(ord($Bytestring{3})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{2})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{1})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{0})), 2, '0', STR_PAD_LEFT); + $GUIDstring = str_pad(dechex(ord($Bytestring[3])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[2])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[1])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[0])), 2, '0', STR_PAD_LEFT); $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{5})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{4})), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[5])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[4])), 2, '0', STR_PAD_LEFT); $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{7})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{6})), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[7])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[6])), 2, '0', STR_PAD_LEFT); $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{8})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{9})), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[8])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[9])), 2, '0', STR_PAD_LEFT); $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{10})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{11})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{12})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{13})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{14})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{15})), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[10])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[11])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[12])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[13])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[14])), 2, '0', STR_PAD_LEFT); + $GUIDstring .= str_pad(dechex(ord($Bytestring[15])), 2, '0', STR_PAD_LEFT); return strtoupper($GUIDstring); } + /** + * @param int $FILETIME + * @param bool $round + * + * @return float|int + */ public static function FILETIMEtoUNIXtime($FILETIME, $round=true) { // FILETIME is a 64-bit unsigned integer representing // the number of 100-nanosecond intervals since January 1, 1601 @@ -1653,6 +1721,11 @@ class getid3_asf extends getid3_handler { return ($FILETIME - 116444736000000000) / 10000000; } + /** + * @param int $WMpictureType + * + * @return string + */ public static function WMpictureTypeLookup($WMpictureType) { static $lookup = null; if ($lookup === null) { @@ -1684,8 +1757,14 @@ class getid3_asf extends getid3_handler { return (isset($lookup[$WMpictureType]) ? $lookup[$WMpictureType] : ''); } + /** + * @param string $asf_header_extension_object_data + * @param int $unhandled_sections + * + * @return array + */ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_data, &$unhandled_sections) { - // http://msdn.microsoft.com/en-us/library/bb643323.aspx + // https://web.archive.org/web/20140419205228/http://msdn.microsoft.com/en-us/library/bb643323.aspx $offset = 0; $objectOffset = 0; @@ -1749,8 +1828,8 @@ class getid3_asf extends getid3_handler { $thisObject['stream_language_id_index'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; - $thisObject['average_time_per_frame'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; + $thisObject['average_time_per_frame'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 8)); + $offset += 8; $thisObject['stream_name_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; @@ -1767,7 +1846,7 @@ class getid3_asf extends getid3_handler { $streamName['stream_name_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; - $streamName['stream_name'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $streamName['stream_name_length'])); + $streamName['stream_name'] = substr($asf_header_extension_object_data, $offset, $streamName['stream_name_length']); $offset += $streamName['stream_name_length']; $thisObject['stream_names'][$i] = $streamName; @@ -1789,7 +1868,7 @@ class getid3_asf extends getid3_handler { $payloadExtensionSystem['extension_system_info_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); $offset += 4; - $payloadExtensionSystem['extension_system_info_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $payloadExtensionSystem['extension_system_info_length'])); + $payloadExtensionSystem['extension_system_info'] = substr($asf_header_extension_object_data, $offset, $payloadExtensionSystem['extension_system_info_length']); $offset += $payloadExtensionSystem['extension_system_info_length']; $thisObject['payload_extension_systems'][$i] = $payloadExtensionSystem; @@ -1797,6 +1876,40 @@ class getid3_asf extends getid3_handler { break; + case GETID3_ASF_Advanced_Mutual_Exclusion_Object: + $thisObject['exclusion_type'] = substr($asf_header_extension_object_data, $offset, 16); + $offset += 16; + $thisObject['exclusion_type_text'] = $this->BytestringToGUID($thisObject['exclusion_type']); + + $thisObject['stream_numbers_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['stream_numbers_count']; $i++) { + $thisObject['stream_numbers'][$i] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + } + + break; + + case GETID3_ASF_Stream_Prioritization_Object: + $thisObject['priority_records_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['priority_records_count']; $i++) { + $priorityRecord = array(); + + $priorityRecord['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $priorityRecord['flags_raw'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $priorityRecord['flags']['mandatory'] = (bool) $priorityRecord['flags_raw'] & 0x00000001; + + $thisObject['priority_records'][$i] = $priorityRecord; + } + + break; + case GETID3_ASF_Padding_Object: // padding, skip it break; @@ -1914,6 +2027,103 @@ class getid3_asf extends getid3_handler { } break; + case GETID3_ASF_Index_Parameters_Object: + $thisObject['index_entry_time_interval'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Media_Object_Index_Parameters_Object: + $thisObject['index_entry_count_interval'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Timecode_Index_Parameters_Object: + // 4.11 Timecode Index Parameters Object (mandatory only if TIMECODE index is present in file, 0 or 1) + // Field name Field type Size (bits) + // Object ID GUID 128 // GUID for the Timecode Index Parameters Object - ASF_Timecode_Index_Parameters_Object + // Object Size QWORD 64 // Specifies the size, in bytes, of the Timecode Index Parameters Object. Valid values are at least 34 bytes. + // Index Entry Count Interval DWORD 32 // This value is ignored for the Timecode Index Parameters Object. + // Index Specifiers Count WORD 16 // Specifies the number of entries in the Index Specifiers list. Valid values are 1 and greater. + // Index Specifiers array of: varies // + // * Stream Number WORD 16 // Specifies the stream number that the Index Specifiers refer to. Valid values are between 1 and 127. + // * Index Type WORD 16 // Specifies the type of index. Values are defined as follows (1 is not a valid value): + // 2 = Nearest Past Media Object - indexes point to the closest data packet containing an entire video frame or the first fragment of a video frame + // 3 = Nearest Past Cleanpoint - indexes point to the closest data packet containing an entire video frame (or first fragment of a video frame) that is a key frame. + // Nearest Past Media Object is the most common value + + $thisObject['index_entry_count_interval'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Compatibility_Object: + $thisObject['profile'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 1)); + $offset += 1; + + $thisObject['mode'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 1)); + $offset += 1; + + break; + default: $unhandled_sections++; if ($this->GUIDname($thisObject['guid_text'])) { @@ -1930,7 +2140,11 @@ class getid3_asf extends getid3_handler { return $HeaderExtensionObjectParsed; } - + /** + * @param int $id + * + * @return string + */ public static function metadataLibraryObjectDataTypeLookup($id) { static $lookup = array( 0x0000 => 'Unicode string', // The data consists of a sequence of Unicode characters @@ -1944,6 +2158,11 @@ class getid3_asf extends getid3_handler { return (isset($lookup[$id]) ? $lookup[$id] : 'invalid'); } + /** + * @param string $data + * + * @return array + */ public function ASF_WMpicture(&$data) { //typedef struct _WMPicture{ // LPWSTR pwszMIMEType; @@ -1994,14 +2213,24 @@ class getid3_asf extends getid3_handler { return $WMpicture; } - - // Remove terminator 00 00 and convert UTF-16LE to Latin-1 + /** + * Remove terminator 00 00 and convert UTF-16LE to Latin-1. + * + * @param string $string + * + * @return string + */ public static function TrimConvert($string) { return trim(getid3_lib::iconv_fallback('UTF-16LE', 'ISO-8859-1', self::TrimTerm($string)), ' '); } - - // Remove terminator 00 00 + /** + * Remove terminator 00 00. + * + * @param string $string + * + * @return string + */ public static function TrimTerm($string) { // remove terminator, only if present (it should be, but...) if (substr($string, -2) === "\x00\x00") { diff --git a/lib/getid3/module.audio-video.bink.php b/lib/getid3/module.audio-video.bink.php index af4b4f8..b59ec67 100644 --- a/lib/getid3/module.audio-video.bink.php +++ b/lib/getid3/module.audio-video.bink.php @@ -1,11 +1,10 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.bink.php // @@ -14,36 +13,38 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_bink extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; -$this->error('Bink / Smacker files not properly processed by this version of getID3() ['.$this->getid3->version().']'); + $this->error('Bink / Smacker files not properly processed by this version of getID3() ['.$this->getid3->version().']'); $this->fseek($info['avdataoffset']); $fileTypeID = $this->fread(3); switch ($fileTypeID) { case 'BIK': return $this->ParseBink(); - break; case 'SMK': return $this->ParseSmacker(); - break; default: $this->error('Expecting "BIK" or "SMK" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($fileTypeID).'"'); return false; - break; } - - return true; - } + /** + * @return bool + */ public function ParseBink() { $info = &$this->getid3->info; $info['fileformat'] = 'bink'; @@ -61,6 +62,9 @@ $this->error('Bink / Smacker files not properly processed by this version of get return true; } + /** + * @return bool + */ public function ParseSmacker() { $info = &$this->getid3->info; $info['fileformat'] = 'smacker'; diff --git a/lib/getid3/module.audio-video.flv.php b/lib/getid3/module.audio-video.flv.php index 661c77c..3b59e59 100644 --- a/lib/getid3/module.audio-video.flv.php +++ b/lib/getid3/module.audio-video.flv.php @@ -1,15 +1,22 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.audio-video.flv.php // +// module for analyzing Shockwave Flash Video files // +// dependencies: NONE // +// // +///////////////////////////////////////////////////////////////// // // // FLV module by Seth Kaufman // // // // * version 0.1 (26 June 2005) // // // -// // // * version 0.1.1 (15 July 2005) // // minor modifications by James Heinrich // // // @@ -43,15 +50,13 @@ // handle GETID3_FLV_VIDEO_VP6FLV_ALPHA // // improved AVCSequenceParameterSetReader::readData() // // by Xander Schouwerwou // -// // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.flv.php // -// module for analyzing Shockwave Flash Video files // -// dependencies: NONE // // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + define('GETID3_FLV_TAG_AUDIO', 8); define('GETID3_FLV_TAG_VIDEO', 9); define('GETID3_FLV_TAG_META', 18); @@ -73,12 +78,21 @@ define('H264_PROFILE_HIGH422', 122); define('H264_PROFILE_HIGH444', 144); define('H264_PROFILE_HIGH444_PREDICTIVE', 244); -class getid3_flv extends getid3_handler { - +class getid3_flv extends getid3_handler +{ const magic = 'FLV'; - public $max_frames = 100000; // break out of the loop if too many frames have been scanned; only scan this many if meta frame does not contain useful duration + /** + * Break out of the loop if too many frames have been scanned; only scan this + * many if meta frame does not contain useful duration. + * + * @var int + */ + public $max_frames = 100000; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -147,6 +161,7 @@ class getid3_flv extends getid3_handler { $info['flv']['video']['videoCodec'] = $LastHeaderByte & 0x07; $FLVvideoHeader = $this->fread(11); + $PictureSizeEnc = array(); if ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H264) { // this code block contributed by: moysevichØgmail*com @@ -332,7 +347,11 @@ class getid3_flv extends getid3_handler { return true; } - + /** + * @param int $id + * + * @return string|false + */ public static function audioFormatLookup($id) { static $lookup = array( 0 => 'Linear PCM, platform endian', @@ -355,6 +374,11 @@ class getid3_flv extends getid3_handler { return (isset($lookup[$id]) ? $lookup[$id] : false); } + /** + * @param int $id + * + * @return int|false + */ public static function audioRateLookup($id) { static $lookup = array( 0 => 5500, @@ -365,6 +389,11 @@ class getid3_flv extends getid3_handler { return (isset($lookup[$id]) ? $lookup[$id] : false); } + /** + * @param int $id + * + * @return int|false + */ public static function audioBitDepthLookup($id) { static $lookup = array( 0 => 8, @@ -373,6 +402,11 @@ class getid3_flv extends getid3_handler { return (isset($lookup[$id]) ? $lookup[$id] : false); } + /** + * @param int $id + * + * @return string|false + */ public static function videoCodecLookup($id) { static $lookup = array( GETID3_FLV_VIDEO_H263 => 'Sorenson H.263', @@ -386,47 +420,84 @@ class getid3_flv extends getid3_handler { } } -class AMFStream { +class AMFStream +{ + /** + * @var string + */ public $bytes; + + /** + * @var int + */ public $pos; + /** + * @param string $bytes + */ public function __construct(&$bytes) { $this->bytes =& $bytes; $this->pos = 0; } - public function readByte() { - return getid3_lib::BigEndian2Int(substr($this->bytes, $this->pos++, 1)); + /** + * @return int + */ + public function readByte() { // 8-bit + return ord(substr($this->bytes, $this->pos++, 1)); } - public function readInt() { + /** + * @return int + */ + public function readInt() { // 16-bit return ($this->readByte() << 8) + $this->readByte(); } - public function readLong() { + /** + * @return int + */ + public function readLong() { // 32-bit return ($this->readByte() << 24) + ($this->readByte() << 16) + ($this->readByte() << 8) + $this->readByte(); } + /** + * @return float|false + */ public function readDouble() { return getid3_lib::BigEndian2Float($this->read(8)); } + /** + * @return string + */ public function readUTF() { $length = $this->readInt(); return $this->read($length); } + /** + * @return string + */ public function readLongUTF() { $length = $this->readLong(); return $this->read($length); } + /** + * @param int $length + * + * @return string + */ public function read($length) { $val = substr($this->bytes, $this->pos, $length); $this->pos += $length; return $val; } + /** + * @return int + */ public function peekByte() { $pos = $this->pos; $val = $this->readByte(); @@ -434,6 +505,9 @@ class AMFStream { return $val; } + /** + * @return int + */ public function peekInt() { $pos = $this->pos; $val = $this->readInt(); @@ -441,6 +515,9 @@ class AMFStream { return $val; } + /** + * @return int + */ public function peekLong() { $pos = $this->pos; $val = $this->readLong(); @@ -448,6 +525,9 @@ class AMFStream { return $val; } + /** + * @return float|false + */ public function peekDouble() { $pos = $this->pos; $val = $this->readDouble(); @@ -455,6 +535,9 @@ class AMFStream { return $val; } + /** + * @return string + */ public function peekUTF() { $pos = $this->pos; $val = $this->readUTF(); @@ -462,6 +545,9 @@ class AMFStream { return $val; } + /** + * @return string + */ public function peekLongUTF() { $pos = $this->pos; $val = $this->readLongUTF(); @@ -470,13 +556,23 @@ class AMFStream { } } -class AMFReader { +class AMFReader +{ + /** + * @var AMFStream + */ public $stream; - public function __construct(&$stream) { - $this->stream =& $stream; + /** + * @param AMFStream $stream + */ + public function __construct(AMFStream $stream) { + $this->stream = $stream; } + /** + * @return mixed + */ public function readData() { $value = null; @@ -506,7 +602,6 @@ class AMFReader { // null case 6: return null; - break; // Mixed array case 8: @@ -547,23 +642,36 @@ class AMFReader { return $value; } + /** + * @return float|false + */ public function readDouble() { return $this->stream->readDouble(); } + /** + * @return bool + */ public function readBoolean() { return $this->stream->readByte() == 1; } + /** + * @return string + */ public function readString() { return $this->stream->readUTF(); } + /** + * @return array + */ public function readObject() { // Get highest numerical index - ignored // $highestIndex = $this->stream->readLong(); $data = array(); + $key = null; while ($key = $this->stream->readUTF()) { $data[$key] = $this->readData(); @@ -576,15 +684,19 @@ class AMFReader { return $data; } + /** + * @return array + */ public function readMixedArray() { // Get highest numerical index - ignored $highestIndex = $this->stream->readLong(); $data = array(); + $key = null; while ($key = $this->stream->readUTF()) { if (is_numeric($key)) { - $key = (float) $key; + $key = (int) $key; } $data[$key] = $this->readData(); } @@ -597,6 +709,9 @@ class AMFReader { return $data; } + /** + * @return array + */ public function readArray() { $length = $this->stream->readLong(); $data = array(); @@ -607,34 +722,61 @@ class AMFReader { return $data; } + /** + * @return float|false + */ public function readDate() { $timestamp = $this->stream->readDouble(); $timezone = $this->stream->readInt(); return $timestamp; } + /** + * @return string + */ public function readLongString() { return $this->stream->readLongUTF(); } + /** + * @return string + */ public function readXML() { return $this->stream->readLongUTF(); } + /** + * @return array + */ public function readTypedObject() { $className = $this->stream->readUTF(); return $this->readObject(); } } -class AVCSequenceParameterSetReader { +class AVCSequenceParameterSetReader +{ + /** + * @var string + */ public $sps; public $start = 0; public $currentBytes = 0; public $currentBits = 0; + + /** + * @var int + */ public $width; + + /** + * @var int + */ public $height; + /** + * @param string $sps + */ public function __construct($sps) { $this->sps = $sps; } @@ -691,18 +833,29 @@ class AVCSequenceParameterSetReader { } } + /** + * @param int $bits + */ public function skipBits($bits) { $newBits = $this->currentBits + $bits; $this->currentBytes += (int)floor($newBits / 8); $this->currentBits = $newBits % 8; } + /** + * @return int + */ public function getBit() { $result = (getid3_lib::BigEndian2Int(substr($this->sps, $this->currentBytes, 1)) >> (7 - $this->currentBits)) & 0x01; $this->skipBits(1); return $result; } + /** + * @param int $bits + * + * @return int + */ public function getBits($bits) { $result = 0; for ($i = 0; $i < $bits; $i++) { @@ -711,6 +864,9 @@ class AVCSequenceParameterSetReader { return $result; } + /** + * @return int + */ public function expGolombUe() { $significantBits = 0; $bit = $this->getBit(); @@ -726,6 +882,9 @@ class AVCSequenceParameterSetReader { return (1 << $significantBits) + $this->getBits($significantBits) - 1; } + /** + * @return int + */ public function expGolombSe() { $result = $this->expGolombUe(); if (($result & 0x01) == 0) { @@ -735,10 +894,16 @@ class AVCSequenceParameterSetReader { } } + /** + * @return int + */ public function getWidth() { return $this->width; } + /** + * @return int + */ public function getHeight() { return $this->height; } diff --git a/lib/getid3/module.audio-video.ivf.php b/lib/getid3/module.audio-video.ivf.php new file mode 100644 index 0000000..c221924 --- /dev/null +++ b/lib/getid3/module.audio-video.ivf.php @@ -0,0 +1,81 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.audio.ivf.php // +// module for analyzing IVF audio-video files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_ivf extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $info['fileformat'] = 'ivf'; + $info['video']['dataformat'] = 'ivf'; + + $this->fseek($info['avdataoffset']); + $IVFheader = $this->fread(32); + + if (substr($IVFheader, 0, 4) == 'DKIF') { + + // https://wiki.multimedia.cx/index.php/IVF + $info['ivf']['header']['signature'] = substr($IVFheader, 0, 4); + $info['ivf']['header']['version'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 4, 2)); // should be 0 + $info['ivf']['header']['headersize'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 6, 2)); + $info['ivf']['header']['fourcc'] = substr($IVFheader, 8, 4); + $info['ivf']['header']['resolution_x'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 12, 2)); + $info['ivf']['header']['resolution_y'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 14, 2)); + $info['ivf']['header']['timebase_numerator'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 16, 4)); + $info['ivf']['header']['timebase_denominator'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 20, 4)); + $info['ivf']['header']['frame_count'] = getid3_lib::LittleEndian2Int(substr($IVFheader, 24, 4)); + //$info['ivf']['header']['reserved'] = substr($IVFheader, 28, 4); + + $info['ivf']['header']['frame_rate'] = (float)getid3_lib::SafeDiv($info['ivf']['header']['timebase_numerator'], $info['ivf']['header']['timebase_denominator']); + + if ($info['ivf']['header']['version'] > 0) { + $this->warning('Expecting IVF header version 0, found version '.$info['ivf']['header']['version'].', results may not be accurate'); + } + + $info['video']['resolution_x'] = $info['ivf']['header']['resolution_x']; + $info['video']['resolution_y'] = $info['ivf']['header']['resolution_y']; + $info['video']['codec'] = $info['ivf']['header']['fourcc']; + + $info['ivf']['frame_count'] = 0; + $timestamp = 0; + while (!$this->feof()) { + if ($frameheader = $this->fread(12)) { + $framesize = getid3_lib::LittleEndian2Int(substr($frameheader, 0, 4)); // size of frame in bytes (not including the 12-byte header) + $timestamp = getid3_lib::LittleEndian2Int(substr($frameheader, 4, 8)); // 64-bit presentation timestamp + $this->fseek($framesize, SEEK_CUR); + $info['ivf']['frame_count']++; + } + } + if ($info['ivf']['frame_count'] && $info['playtime_seconds']) { + $info['playtime_seconds'] = $timestamp / 100000; + $info['video']['frame_rate'] = (float) $info['ivf']['frame_count'] / $info['playtime_seconds']; + } + + } else { + $this->error('Expecting "DKIF" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($IVFheader, 0, 4)).'"'); + return false; + } + + return true; + } + +} diff --git a/lib/getid3/module.audio-video.matroska.php b/lib/getid3/module.audio-video.matroska.php index 0852969..eb5febf 100644 --- a/lib/getid3/module.audio-video.matroska.php +++ b/lib/getid3/module.audio-video.matroska.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.matriska.php // @@ -14,6 +14,9 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} define('EBML_ID_CHAPTERS', 0x0043A770); // [10][43][A7][70] -- A system to define basic menus and partition data. For more detailed information, look at the Chapters Explanation. define('EBML_ID_SEEKHEAD', 0x014D9B74); // [11][4D][9B][74] -- Contains the position of other level 1 elements. @@ -215,17 +218,33 @@ define('EBML_ID_CLUSTERREFERENCEVIRTUAL', 0x7D); // [FD] -- */ class getid3_matroska extends getid3_handler { - // public options - public static $hide_clusters = true; // if true, do not return information about CLUSTER chunks, since there's a lot of them and they're not usually useful [default: TRUE] - public static $parse_whole_file = false; // true to parse the whole file, not only header [default: FALSE] + /** + * If true, do not return information about CLUSTER chunks, since there's a lot of them + * and they're not usually useful [default: TRUE]. + * + * @var bool + */ + public $hide_clusters = true; - // private parser settings/placeholders + /** + * True to parse the whole file, not only header [default: FALSE]. + * + * @var bool + */ + public $parse_whole_file = false; + + /* + * Private parser settings/placeholders. + */ private $EBMLbuffer = ''; private $EBMLbuffer_offset = 0; private $EBMLbuffer_length = 0; private $current_offset = 0; private $unuseful_elements = array(EBML_ID_CRC32, EBML_ID_VOID); + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -273,12 +292,12 @@ class getid3_matroska extends getid3_handler $track_info['display_x'] = (isset($trackarray['DisplayWidth']) ? $trackarray['DisplayWidth'] : $trackarray['PixelWidth']); $track_info['display_y'] = (isset($trackarray['DisplayHeight']) ? $trackarray['DisplayHeight'] : $trackarray['PixelHeight']); - if (isset($trackarray['PixelCropBottom'])) { $track_info['crop_bottom'] = $trackarray['PixelCropBottom']; } - if (isset($trackarray['PixelCropTop'])) { $track_info['crop_top'] = $trackarray['PixelCropTop']; } - if (isset($trackarray['PixelCropLeft'])) { $track_info['crop_left'] = $trackarray['PixelCropLeft']; } - if (isset($trackarray['PixelCropRight'])) { $track_info['crop_right'] = $trackarray['PixelCropRight']; } - if (isset($trackarray['DefaultDuration'])) { $track_info['frame_rate'] = round(1000000000 / $trackarray['DefaultDuration'], 3); } - if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } + if (isset($trackarray['PixelCropBottom'])) { $track_info['crop_bottom'] = $trackarray['PixelCropBottom']; } + if (isset($trackarray['PixelCropTop'])) { $track_info['crop_top'] = $trackarray['PixelCropTop']; } + if (isset($trackarray['PixelCropLeft'])) { $track_info['crop_left'] = $trackarray['PixelCropLeft']; } + if (isset($trackarray['PixelCropRight'])) { $track_info['crop_right'] = $trackarray['PixelCropRight']; } + if (!empty($trackarray['DefaultDuration'])) { $track_info['frame_rate'] = round(1000000000 / $trackarray['DefaultDuration'], 3); } + if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } switch ($trackarray['CodecID']) { case 'V_MS/VFW/FOURCC': @@ -313,7 +332,7 @@ class getid3_matroska extends getid3_handler break;*/ } - $info['video']['streams'][] = $track_info; + $info['video']['streams'][$trackarray['TrackUID']] = $track_info; break; case 2: // Audio @@ -326,7 +345,7 @@ class getid3_matroska extends getid3_handler switch ($trackarray['CodecID']) { case 'A_PCM/INT/LIT': case 'A_PCM/INT/BIG': - $track_info['bitrate'] = $trackarray['SamplingFrequency'] * $trackarray['Channels'] * $trackarray['BitDepth']; + $track_info['bitrate'] = $track_info['sample_rate'] * $track_info['channels'] * $trackarray['BitDepth']; break; case 'A_AC3': @@ -346,7 +365,7 @@ class getid3_matroska extends getid3_handler // create temp instance $getid3_temp = new getID3(); if ($track_info['dataformat'] != 'flac') { - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); } $getid3_temp->info['avdataoffset'] = $info['matroska']['track_data_offsets'][$trackarray['TrackNumber']]['offset']; if ($track_info['dataformat'][0] == 'm' || $track_info['dataformat'] == 'flac') { @@ -366,8 +385,8 @@ class getid3_matroska extends getid3_handler if (!empty($getid3_temp->info[$header_data_key])) { $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $getid3_temp->info[$header_data_key]; if (isset($getid3_temp->info['audio']) && is_array($getid3_temp->info['audio'])) { - foreach ($getid3_temp->info['audio'] as $key => $value) { - $track_info[$key] = $value; + foreach ($getid3_temp->info['audio'] as $sub_key => $value) { + $track_info[$sub_key] = $value; } } } @@ -421,8 +440,8 @@ class getid3_matroska extends getid3_handler if (!empty($getid3_temp->info['ogg'])) { $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $getid3_temp->info['ogg']; if (isset($getid3_temp->info['audio']) && is_array($getid3_temp->info['audio'])) { - foreach ($getid3_temp->info['audio'] as $key => $value) { - $track_info[$key] = $value; + foreach ($getid3_temp->info['audio'] as $sub_key => $value) { + $track_info[$sub_key] = $value; } } } @@ -449,9 +468,9 @@ class getid3_matroska extends getid3_handler getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); $parsed = getid3_riff::parseWAVEFORMATex($trackarray['CodecPrivate']); - foreach ($parsed as $key => $value) { - if ($key != 'raw') { - $track_info[$key] = $value; + foreach ($parsed as $sub_key => $value) { + if ($sub_key != 'raw') { + $track_info[$sub_key] = $value; } } $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $parsed; @@ -462,7 +481,7 @@ class getid3_matroska extends getid3_handler break; } - $info['audio']['streams'][] = $track_info; + $info['audio']['streams'][$trackarray['TrackUID']] = $track_info; break; } } @@ -493,9 +512,36 @@ class getid3_matroska extends getid3_handler unset($info['mime_type']); } + // use _STATISTICS_TAGS if available to set audio/video bitrates + if (!empty($info['matroska']['tags'])) { + $_STATISTICS_byTrackUID = array(); + foreach ($info['matroska']['tags'] as $key1 => $value1) { + if (!empty($value1['Targets']['TagTrackUID'][0]) && !empty($value1['SimpleTag'])) { + foreach ($value1['SimpleTag'] as $key2 => $value2) { + if (!empty($value2['TagName']) && isset($value2['TagString'])) { + $_STATISTICS_byTrackUID[$value1['Targets']['TagTrackUID'][0]][$value2['TagName']] = $value2['TagString']; + } + } + } + } + foreach (array('audio','video') as $avtype) { + if (!empty($info[$avtype]['streams'])) { + foreach ($info[$avtype]['streams'] as $trackUID => $trackdata) { + if (!isset($trackdata['bitrate']) && !empty($_STATISTICS_byTrackUID[$trackUID]['BPS'])) { + $info[$avtype]['streams'][$trackUID]['bitrate'] = (int) $_STATISTICS_byTrackUID[$trackUID]['BPS']; + @$info[$avtype]['bitrate'] += $info[$avtype]['streams'][$trackUID]['bitrate']; + } + } + } + } + } + return true; } + /** + * @param array $info + */ private function parseEBML(&$info) { // http://www.matroska.org/technical/specs/index.html#EBMLBasics $this->current_offset = $info['avdataoffset']; @@ -540,7 +586,7 @@ class getid3_matroska extends getid3_handler $info['matroska']['segment'][0]['length'] = $top_element['length']; while ($this->getEBMLelement($element_data, $top_element['end'])) { - if ($element_data['id'] != EBML_ID_CLUSTER || !self::$hide_clusters) { // collect clusters only if required + if ($element_data['id'] != EBML_ID_CLUSTER || !$this->hide_clusters) { // collect clusters only if required $info['matroska']['segments'][] = $element_data; } switch ($element_data['id']) { @@ -572,7 +618,7 @@ class getid3_matroska extends getid3_handler $this->warning('seek_entry[target_id] unexpectedly not set at '.$seek_entry['offset']); break; } - if (($seek_entry['target_id'] != EBML_ID_CLUSTER) || !self::$hide_clusters) { // collect clusters only if required + if (($seek_entry['target_id'] != EBML_ID_CLUSTER) || !$this->hide_clusters) { // collect clusters only if required $info['matroska']['seek'][] = $seek_entry; } break; @@ -595,8 +641,10 @@ class getid3_matroska extends getid3_handler while ($this->getEBMLelement($subelement, $track_entry['end'], array(EBML_ID_VIDEO, EBML_ID_AUDIO, EBML_ID_CONTENTENCODINGS, EBML_ID_CODECPRIVATE))) { switch ($subelement['id']) { - case EBML_ID_TRACKNUMBER: case EBML_ID_TRACKUID: + $track_entry[$subelement['id_name']] = getid3_lib::PrintHexBytes($subelement['data'], true, false); + break; + case EBML_ID_TRACKNUMBER: case EBML_ID_TRACKTYPE: case EBML_ID_MINCACHE: case EBML_ID_MAXCACHE: @@ -857,7 +905,7 @@ class getid3_matroska extends getid3_handler break; case EBML_ID_CUES: // A top-level element to speed seeking access. All entries are local to the segment. Should be mandatory for non "live" streams. - if (self::$hide_clusters) { // do not parse cues if hide clusters is "ON" till they point to clusters anyway + if ($this->hide_clusters) { // do not parse cues if hide clusters is "ON" till they point to clusters anyway $this->current_offset = $element_data['end']; break; } @@ -944,7 +992,7 @@ class getid3_matroska extends getid3_handler case EBML_ID_TAGEDITIONUID: case EBML_ID_TAGCHAPTERUID: case EBML_ID_TAGATTACHMENTUID: - $targets_entry[$sub_sub_subelement['id_name']][] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); + $targets_entry[$sub_sub_subelement['id_name']][] = getid3_lib::PrintHexBytes($sub_sub_subelement['data'], true, false); break; default: @@ -1198,12 +1246,12 @@ class getid3_matroska extends getid3_handler } $this->current_offset = $subelement['end']; } - if (!self::$hide_clusters) { + if (!$this->hide_clusters) { $info['matroska']['cluster'][] = $cluster_entry; } // check to see if all the data we need exists already, if so, break out of the loop - if (!self::$parse_whole_file) { + if (!$this->parse_whole_file) { if (isset($info['matroska']['info']) && is_array($info['matroska']['info'])) { if (isset($info['matroska']['tracks']['tracks']) && is_array($info['matroska']['tracks']['tracks'])) { if (count($info['matroska']['track_data_offsets']) == count($info['matroska']['tracks']['tracks'])) { @@ -1228,6 +1276,11 @@ class getid3_matroska extends getid3_handler } } + /** + * @param int $min_data + * + * @return bool + */ private function EnsureBufferHasEnoughData($min_data=1024) { if (($this->current_offset - $this->EBMLbuffer_offset) >= ($this->EBMLbuffer_length - $min_data)) { $read_bytes = max($min_data, $this->getid3->fread_buffer_size()); @@ -1249,6 +1302,9 @@ class getid3_matroska extends getid3_handler return true; } + /** + * @return int|float|false + */ private function readEBMLint() { $actual_offset = $this->current_offset - $this->EBMLbuffer_offset; @@ -1281,6 +1337,12 @@ class getid3_matroska extends getid3_handler return $int_value; } + /** + * @param int $length + * @param bool $check_buffer + * + * @return string|false + */ private function readEBMLelementData($length, $check_buffer=false) { if ($check_buffer && !$this->EnsureBufferHasEnoughData($length)) { return false; @@ -1290,6 +1352,13 @@ class getid3_matroska extends getid3_handler return $data; } + /** + * @param array $element + * @param int $parent_end + * @param array|bool $get_data + * + * @return bool + */ private function getEBMLelement(&$element, $parent_end, $get_data=false) { if ($this->current_offset >= $parent_end) { return false; @@ -1326,6 +1395,11 @@ class getid3_matroska extends getid3_handler return true; } + /** + * @param string $type + * @param int $line + * @param array $element + */ private function unhandledElement($type, $line, $element) { // warn only about unknown and missed elements, not about unuseful if (!in_array($element['id'], $this->unuseful_elements)) { @@ -1338,6 +1412,11 @@ class getid3_matroska extends getid3_handler } } + /** + * @param array $SimpleTagArray + * + * @return bool + */ private function ExtractCommentsSimpleTag($SimpleTagArray) { if (!empty($SimpleTagArray['SimpleTag'])) { foreach ($SimpleTagArray['SimpleTag'] as $SimpleTagKey => $SimpleTagData) { @@ -1353,6 +1432,11 @@ class getid3_matroska extends getid3_handler return true; } + /** + * @param int $parent_end + * + * @return array + */ private function HandleEMBLSimpleTag($parent_end) { $simpletag_entry = array(); @@ -1383,6 +1467,13 @@ class getid3_matroska extends getid3_handler return $simpletag_entry; } + /** + * @param array $element + * @param int $block_type + * @param array $info + * + * @return array + */ private function HandleEMBLClusterBlock($element, $block_type, &$info) { // http://www.matroska.org/technical/specs/index.html#block_structure // http://www.matroska.org/technical/specs/index.html#simpleblock_structure @@ -1446,6 +1537,11 @@ class getid3_matroska extends getid3_handler return $block_data; } + /** + * @param string $EBMLstring + * + * @return int|float|false + */ private static function EBML2Int($EBMLstring) { // http://matroska.org/specs/ @@ -1488,12 +1584,22 @@ class getid3_matroska extends getid3_handler return getid3_lib::BigEndian2Int($EBMLstring); } + /** + * @param int $EBMLdatestamp + * + * @return float + */ private static function EBMLdate2unix($EBMLdatestamp) { // Date - signed 8 octets integer in nanoseconds with 0 indicating the precise beginning of the millennium (at 2001-01-01T00:00:00,000000000 UTC) // 978307200 == mktime(0, 0, 0, 1, 1, 2001) == January 1, 2001 12:00:00am UTC return round(($EBMLdatestamp / 1000000000) + 978307200); } + /** + * @param int $target_type + * + * @return string|int + */ public static function TargetTypeValue($target_type) { // http://www.matroska.org/technical/specs/tagging/index.html static $TargetTypeValue = array(); @@ -1509,6 +1615,11 @@ class getid3_matroska extends getid3_handler return (isset($TargetTypeValue[$target_type]) ? $TargetTypeValue[$target_type] : $target_type); } + /** + * @param int $lacingtype + * + * @return string|int + */ public static function BlockLacingType($lacingtype) { // http://matroska.org/technical/specs/index.html#block_structure static $BlockLacingType = array(); @@ -1521,6 +1632,11 @@ class getid3_matroska extends getid3_handler return (isset($BlockLacingType[$lacingtype]) ? $BlockLacingType[$lacingtype] : $lacingtype); } + /** + * @param string $codecid + * + * @return string + */ public static function CodecIDtoCommonName($codecid) { // http://www.matroska.org/technical/specs/codecid/index.html static $CodecIDlist = array(); @@ -1557,6 +1673,11 @@ class getid3_matroska extends getid3_handler return (isset($CodecIDlist[$codecid]) ? $CodecIDlist[$codecid] : $codecid); } + /** + * @param int $value + * + * @return string + */ private static function EBMLidName($value) { static $EBMLidList = array(); if (empty($EBMLidList)) { @@ -1755,6 +1876,11 @@ class getid3_matroska extends getid3_handler return (isset($EBMLidList[$value]) ? $EBMLidList[$value] : dechex($value)); } + /** + * @param int $value + * + * @return string + */ public static function displayUnit($value) { // http://www.matroska.org/technical/specs/index.html#DisplayUnit static $units = array( @@ -1766,8 +1892,14 @@ class getid3_matroska extends getid3_handler return (isset($units[$value]) ? $units[$value] : 'unknown'); } + /** + * @param array $streams + * + * @return array + */ private static function getDefaultStreamInfo($streams) { + $stream = array(); foreach (array_reverse($streams) as $stream) { if ($stream['default']) { break; diff --git a/lib/getid3/module.audio-video.mpeg.php b/lib/getid3/module.audio-video.mpeg.php index 044481f..fe16e32 100644 --- a/lib/getid3/module.audio-video.mpeg.php +++ b/lib/getid3/module.audio-video.mpeg.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.mpeg.php // @@ -14,9 +14,13 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); -class getid3_mpeg extends getid3_handler { +class getid3_mpeg extends getid3_handler +{ const START_CODE_BASE = "\x00\x00\x01"; const VIDEO_PICTURE_START = "\x00\x00\x01\x00"; @@ -28,7 +32,9 @@ class getid3_mpeg extends getid3_handler { const VIDEO_GROUP_START = "\x00\x00\x01\xB8"; const AUDIO_START = "\x00\x00\x01\xC0"; - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -87,11 +93,9 @@ class getid3_mpeg extends getid3_handler { break; case 0xB3: // sequence_header_code - /* - Note: purposely doing the less-pretty (and probably a bit slower) method of using string of bits rather than bitwise operations. - Mostly because PHP 32-bit doesn't handle unsigned integers well for bitwise operation. - Also the MPEG stream is designed as a bitstream and often doesn't align nicely with byte boundaries. - */ + // Note: purposely doing the less-pretty (and probably a bit slower) method of using string of bits rather than bitwise operations. + // Mostly because PHP 32-bit doesn't handle unsigned integers well for bitwise operation. + // Also the MPEG stream is designed as a bitstream and often doesn't align nicely with byte boundaries. $info['video']['codec'] = 'MPEG-1'; // will be updated if extension_start_code found $bitstream = getid3_lib::BigEndian2Bin(substr($MPEGstreamData, $StartCodeOffset + 4, 8)); @@ -122,8 +126,8 @@ class getid3_mpeg extends getid3_handler { } } - $info['mpeg']['video']['pixel_aspect_ratio'] = self::videoAspectRatioLookup($info['mpeg']['video']['raw']['aspect_ratio_information']); - $info['mpeg']['video']['pixel_aspect_ratio_text'] = self::videoAspectRatioTextLookup($info['mpeg']['video']['raw']['aspect_ratio_information']); + $info['mpeg']['video']['pixel_aspect_ratio'] = self::videoAspectRatioLookup($info['mpeg']['video']['raw']['aspect_ratio_information']); // may be overridden later if file turns out to be MPEG-2 + $info['mpeg']['video']['pixel_aspect_ratio_text'] = self::videoAspectRatioTextLookup($info['mpeg']['video']['raw']['aspect_ratio_information']); // may be overridden later if file turns out to be MPEG-2 $info['mpeg']['video']['frame_rate'] = self::videoFramerateLookup($info['mpeg']['video']['raw']['frame_rate_code']); if ($info['mpeg']['video']['raw']['bitrate'] == 0x3FFFF) { // 18 set bits = VBR //$this->warning('This version of getID3() ['.$this->getid3->version().'] cannot determine average bitrate of VBR MPEG video files'); @@ -169,6 +173,16 @@ class getid3_mpeg extends getid3_handler { $info['video']['interlaced'] = !$info['mpeg']['video']['raw']['progressive_sequence']; $info['mpeg']['video']['interlaced'] = !$info['mpeg']['video']['raw']['progressive_sequence']; $info['mpeg']['video']['chroma_format'] = self::chromaFormatTextLookup($info['mpeg']['video']['raw']['chroma_format']); + + if (isset($info['mpeg']['video']['raw']['aspect_ratio_information'])) { + // MPEG-2 defines the aspect ratio flag differently from MPEG-1, but the MPEG-2 extension start code may occur after we've already looked up the aspect ratio assuming it was MPEG-1, so re-lookup assuming MPEG-2 + // This must be done after the extended size is known, so the display aspect ratios can be converted to pixel aspect ratios. + $info['mpeg']['video']['pixel_aspect_ratio'] = self::videoAspectRatioLookup($info['mpeg']['video']['raw']['aspect_ratio_information'], 2, $info['video']['resolution_x'], $info['video']['resolution_y']); + $info['mpeg']['video']['pixel_aspect_ratio_text'] = self::videoAspectRatioTextLookup($info['mpeg']['video']['raw']['aspect_ratio_information'], 2); + $info['video']['pixel_aspect_ratio'] = $info['mpeg']['video']['pixel_aspect_ratio']; + $info['video']['pixel_aspect_ratio_text'] = $info['mpeg']['video']['pixel_aspect_ratio_text']; + } + break; case 2: // 0010 Sequence Display Extension ID @@ -258,7 +272,7 @@ class getid3_mpeg extends getid3_handler { case 0xB8: // group_of_pictures_header $GOPcounter++; - if ($info['mpeg']['video']['bitrate_mode'] == 'vbr') { + if (!empty($info['mpeg']['video']['bitrate_mode']) && ($info['mpeg']['video']['bitrate_mode'] == 'vbr')) { $bitstream = getid3_lib::BigEndian2Bin(substr($MPEGstreamData, $StartCodeOffset + 4, 4)); // 27 bits needed for group_of_pictures_header $bitstreamoffset = 0; @@ -274,7 +288,7 @@ class getid3_mpeg extends getid3_handler { $GOPheader['closed_gop'] = self::readBitsFromStream($bitstream, $bitstreamoffset, 1); // 1 bit flag: closed_gop $GOPheader['broken_link'] = self::readBitsFromStream($bitstream, $bitstreamoffset, 1); // 1 bit flag: broken_link - $time_code_separator = ($GOPheader['drop_frame_flag'] ? ';' : ':'); // While non-drop time code is displayed with colons separating the digit pairs—"HH:MM:SS:FF"—drop frame is usually represented with a semi-colon (;) or period (.) as the divider between all the digit pairs—"HH;MM;SS;FF", "HH.MM.SS.FF" + $time_code_separator = ($GOPheader['drop_frame_flag'] ? ';' : ':'); // While non-drop time code is displayed with colons separating the digit pairs "HH:MM:SS:FF" drop frame is usually represented with a semi-colon (;) or period (.) as the divider between all the digit pairs "HH;MM;SS;FF", "HH.MM.SS.FF" $GOPheader['time_code'] = sprintf('%02d'.$time_code_separator.'%02d'.$time_code_separator.'%02d'.$time_code_separator.'%02d', $GOPheader['time_code_hours'], $GOPheader['time_code_minutes'], $GOPheader['time_code_seconds'], $GOPheader['time_code_pictures']); $info['mpeg']['group_of_pictures'][] = $GOPheader; @@ -380,7 +394,7 @@ $PackedElementaryStream['additional_header_bytes'] = $additional_header_bytes; $info['mpeg']['packed_elementary_streams'][$PackedElementaryStream['stream_type']][$PackedElementaryStream['stream_id']][] = $PackedElementaryStream; */ $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info = $info; $getid3_mp3 = new getid3_mp3($getid3_temp); for ($i = 0; $i <= 7; $i++) { @@ -491,9 +505,9 @@ echo 'average_File_bitrate = '.number_format(array_sum($vbr_bitrates) / count($v $last_GOP_id = max(array_keys($FramesByGOP)); $frames_in_last_GOP = count($FramesByGOP[$last_GOP_id]); $gopdata = &$info['mpeg']['group_of_pictures'][$last_GOP_id]; - $info['playtime_seconds'] = ($gopdata['time_code_hours'] * 3600) + ($gopdata['time_code_minutes'] * 60) + $gopdata['time_code_seconds'] + (($gopdata['time_code_pictures'] + $frames_in_last_GOP + 1) / $info['mpeg']['video']['frame_rate']); + $info['playtime_seconds'] = ($gopdata['time_code_hours'] * 3600) + ($gopdata['time_code_minutes'] * 60) + $gopdata['time_code_seconds'] + getid3_lib::SafeDiv($gopdata['time_code_pictures'] + $frames_in_last_GOP + 1, $info['mpeg']['video']['frame_rate']); if (!isset($info['video']['bitrate'])) { - $overall_bitrate = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['playtime_seconds']; + $overall_bitrate = getid3_lib::SafeDiv($info['avdataend'] - $info['avdataoffset'] * 8, $info['playtime_seconds']); $info['video']['bitrate'] = $overall_bitrate - (isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : 0); } unset($info['mpeg']['group_of_pictures']); @@ -502,6 +516,14 @@ echo 'average_File_bitrate = '.number_format(array_sum($vbr_bitrates) / count($v return true; } + /** + * @param string $bitstream + * @param int $bitstreamoffset + * @param int $bits_to_read + * @param bool $return_singlebit_as_boolean + * + * @return bool|int + */ private function readBitsFromStream(&$bitstream, &$bitstreamoffset, $bits_to_read, $return_singlebit_as_boolean=true) { $return = bindec(substr($bitstream, $bitstreamoffset, $bits_to_read)); $bitstreamoffset += $bits_to_read; @@ -511,7 +533,12 @@ echo 'average_File_bitrate = '.number_format(array_sum($vbr_bitrates) / count($v return $return; } - + /** + * @param int $VideoBitrate + * @param int $AudioBitrate + * + * @return float|int + */ public static function systemNonOverheadPercentage($VideoBitrate, $AudioBitrate) { $OverheadPercentage = 0; @@ -519,6 +546,7 @@ echo 'average_File_bitrate = '.number_format(array_sum($vbr_bitrates) / count($v $VideoBitrate = max(min($VideoBitrate / 1000, 10000), 10); // limit to range of 10kbps - 10Mbps (beyond that curves flatten anyways, no big loss) + $OverheadMultiplierByBitrate = array(); //OMBB[audiobitrate] = array(video-10kbps, video-100kbps, video-1000kbps, video-10000kbps) $OverheadMultiplierByBitrate[32] = array(0, 0.9676287944368530, 0.9802276264360310, 0.9844916183244460, 0.9852821845179940); $OverheadMultiplierByBitrate[48] = array(0, 0.9779100089209830, 0.9787770035359320, 0.9846738664076130, 0.9852683013799960); @@ -563,44 +591,93 @@ echo 'average_File_bitrate = '.number_format(array_sum($vbr_bitrates) / count($v return $OverheadPercentage; } - + /** + * @param int $rawframerate + * + * @return float + */ public static function videoFramerateLookup($rawframerate) { $lookup = array(0, 23.976, 24, 25, 29.97, 30, 50, 59.94, 60); - return (isset($lookup[$rawframerate]) ? (float) $lookup[$rawframerate] : (float) 0); + return (float) (isset($lookup[$rawframerate]) ? $lookup[$rawframerate] : 0); } - public static function videoAspectRatioLookup($rawaspectratio) { - $lookup = array(0, 1, 0.6735, 0.7031, 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, 1.0255, 1.0695, 1.0950, 1.1575, 1.2015, 0); - return (isset($lookup[$rawaspectratio]) ? (float) $lookup[$rawaspectratio] : (float) 0); + /** + * @param int $rawaspectratio + * @param int $mpeg_version + * @param int $width + * @param int $height + * + * @return float + */ + public static function videoAspectRatioLookup($rawaspectratio, $mpeg_version=1, $width=0, $height=0) { + $lookup = array( + 1 => array(0, 1, 0.6735, 0.7031, 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, 1.0255, 1.0695, 1.0950, 1.1575, 1.2015, 0), + 2 => array(0, 1, 1.3333, 1.7778, 2.2100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + ); + $ratio = (float) (isset($lookup[$mpeg_version][$rawaspectratio]) ? $lookup[$mpeg_version][$rawaspectratio] : 0); + if ($mpeg_version == 2 && $ratio != 1 && $width != 0) { + // Calculate pixel aspect ratio from MPEG-2 display aspect ratio + $ratio = $ratio * $height / $width; + } + return $ratio; } - public static function videoAspectRatioTextLookup($rawaspectratio) { - $lookup = array('forbidden', 'square pixels', '0.6735', '16:9, 625 line, PAL', '0.7615', '0.8055', '16:9, 525 line, NTSC', '0.8935', '4:3, 625 line, PAL, CCIR601', '0.9815', '1.0255', '1.0695', '4:3, 525 line, NTSC, CCIR601', '1.1575', '1.2015', 'reserved'); - return (isset($lookup[$rawaspectratio]) ? $lookup[$rawaspectratio] : ''); + /** + * @param int $rawaspectratio + * @param int $mpeg_version + * + * @return string + */ + public static function videoAspectRatioTextLookup($rawaspectratio, $mpeg_version=1) { + $lookup = array( + 1 => array('forbidden', 'square pixels', '0.6735', '16:9, 625 line, PAL', '0.7615', '0.8055', '16:9, 525 line, NTSC', '0.8935', '4:3, 625 line, PAL, CCIR601', '0.9815', '1.0255', '1.0695', '4:3, 525 line, NTSC, CCIR601', '1.1575', '1.2015', 'reserved'), + 2 => array('forbidden', 'square pixels', '4:3', '16:9', '2.21:1', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved', 'reserved'), // http://dvd.sourceforge.net/dvdinfo/mpeghdrs.html + ); + return (isset($lookup[$mpeg_version][$rawaspectratio]) ? $lookup[$mpeg_version][$rawaspectratio] : ''); } + /** + * @param int $video_format + * + * @return string + */ public static function videoFormatTextLookup($video_format) { // ISO/IEC 13818-2, section 6.3.6, Table 6-6. Meaning of video_format $lookup = array('component', 'PAL', 'NTSC', 'SECAM', 'MAC', 'Unspecified video format', 'reserved(6)', 'reserved(7)'); return (isset($lookup[$video_format]) ? $lookup[$video_format] : ''); } + /** + * @param int $scalable_mode + * + * @return string + */ public static function scalableModeTextLookup($scalable_mode) { // ISO/IEC 13818-2, section 6.3.8, Table 6-10. Definition of scalable_mode $lookup = array('data partitioning', 'spatial scalability', 'SNR scalability', 'temporal scalability'); return (isset($lookup[$scalable_mode]) ? $lookup[$scalable_mode] : ''); } + /** + * @param int $picture_structure + * + * @return string + */ public static function pictureStructureTextLookup($picture_structure) { // ISO/IEC 13818-2, section 6.3.11, Table 6-14 Meaning of picture_structure $lookup = array('reserved', 'Top Field', 'Bottom Field', 'Frame picture'); return (isset($lookup[$picture_structure]) ? $lookup[$picture_structure] : ''); } + /** + * @param int $chroma_format + * + * @return string + */ public static function chromaFormatTextLookup($chroma_format) { // ISO/IEC 13818-2, section 6.3.11, Table 6-14 Meaning of picture_structure $lookup = array('reserved', '4:2:0', '4:2:2', '4:4:4'); return (isset($lookup[$chroma_format]) ? $lookup[$chroma_format] : ''); } -} \ No newline at end of file +} diff --git a/lib/getid3/module.audio-video.nsv.php b/lib/getid3/module.audio-video.nsv.php index eab601b..b59642a 100644 --- a/lib/getid3/module.audio-video.nsv.php +++ b/lib/getid3/module.audio-video.nsv.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.nsv.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_nsv extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -49,7 +54,6 @@ class getid3_nsv extends getid3_handler default: $this->error('Expecting "NSVs" or "NSVf" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($NSVheader).'"'); return false; - break; } if (!isset($info['nsv']['NSVf'])) { @@ -59,6 +63,11 @@ class getid3_nsv extends getid3_handler return true; } + /** + * @param int $fileoffset + * + * @return bool + */ public function getNSVsHeaderFilepointer($fileoffset) { $info = &$this->getid3->info; $this->fseek($fileoffset); @@ -132,6 +141,12 @@ class getid3_nsv extends getid3_handler return true; } + /** + * @param int $fileoffset + * @param bool $getTOCoffsets + * + * @return bool + */ public function getNSVfHeaderFilepointer($fileoffset, $getTOCoffsets=false) { $info = &$this->getid3->info; $this->fseek($fileoffset); @@ -200,23 +215,27 @@ class getid3_nsv extends getid3_handler } $info['playtime_seconds'] = $info['nsv']['NSVf']['playtime_ms'] / 1000; - $info['bitrate'] = ($info['nsv']['NSVf']['file_size'] * 8) / $info['playtime_seconds']; + $info['bitrate'] = getid3_lib::SafeDiv($info['nsv']['NSVf']['file_size'] * 8, $info['playtime_seconds']); return true; } - + /** + * @param int $framerateindex + * + * @return float|false + */ public static function NSVframerateLookup($framerateindex) { if ($framerateindex <= 127) { return (float) $framerateindex; } static $NSVframerateLookup = array(); if (empty($NSVframerateLookup)) { - $NSVframerateLookup[129] = (float) 29.970; - $NSVframerateLookup[131] = (float) 23.976; - $NSVframerateLookup[133] = (float) 14.985; - $NSVframerateLookup[197] = (float) 59.940; - $NSVframerateLookup[199] = (float) 47.952; + $NSVframerateLookup[129] = 29.970; + $NSVframerateLookup[131] = 23.976; + $NSVframerateLookup[133] = 14.985; + $NSVframerateLookup[197] = 59.940; + $NSVframerateLookup[199] = 47.952; } return (isset($NSVframerateLookup[$framerateindex]) ? $NSVframerateLookup[$framerateindex] : false); } diff --git a/lib/getid3/module.audio-video.quicktime.php b/lib/getid3/module.audio-video.quicktime.php index 3651c4f..433f468 100644 --- a/lib/getid3/module.audio-video.quicktime.php +++ b/lib/getid3/module.audio-video.quicktime.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.quicktime.php // @@ -15,15 +15,32 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); // needed for ISO 639-2 language code lookup class getid3_quicktime extends getid3_handler { - public $ReturnAtomData = true; + /** audio-video.quicktime + * return all parsed data from all atoms if true, otherwise just returned parsed metadata + * + * @var bool + */ + public $ReturnAtomData = false; + + /** audio-video.quicktime + * return all parsed data from all atoms if true, otherwise just returned parsed metadata + * + * @var bool + */ public $ParseAllPossibleAtoms = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -35,7 +52,7 @@ class getid3_quicktime extends getid3_handler $offset = 0; $atomcounter = 0; - $atom_data_read_buffer_size = max($this->getid3->option_fread_buffer_size * 1024, ($info['php_memory_limit'] ? round($info['php_memory_limit'] / 4) : 1024)); // set read buffer to 25% of PHP memory limit (if one is specified), otherwise use option_fread_buffer_size [default: 32MB] + $atom_data_read_buffer_size = $info['php_memory_limit'] ? round($info['php_memory_limit'] / 4) : $this->getid3->option_fread_buffer_size * 1024; // set read buffer to 25% of PHP memory limit (if one is specified), otherwise use option_fread_buffer_size [default: 32MB] while ($offset < $info['avdataend']) { if (!getid3_lib::intValueSupported($offset)) { $this->error('Unable to parse atom at offset '.$offset.' because beyond '.round(PHP_INT_MAX / 1073741824).'GB limit of PHP filesystem functions'); @@ -44,31 +61,47 @@ class getid3_quicktime extends getid3_handler $this->fseek($offset); $AtomHeader = $this->fread(8); + // https://github.com/JamesHeinrich/getID3/issues/382 + // Atom sizes are stored as 32-bit number in most cases, but sometimes (notably for "mdat") + // a 64-bit value is required, in which case the normal 32-bit size field is set to 0x00000001 + // and the 64-bit "real" size value is the next 8 bytes. + $atom_size_extended_bytes = 0; $atomsize = getid3_lib::BigEndian2Int(substr($AtomHeader, 0, 4)); $atomname = substr($AtomHeader, 4, 4); - - // 64-bit MOV patch by jlegateØktnc*com if ($atomsize == 1) { - $atomsize = getid3_lib::BigEndian2Int($this->fread(8)); + $atom_size_extended_bytes = 8; + $atomsize = getid3_lib::BigEndian2Int($this->fread($atom_size_extended_bytes)); } - $info['quicktime'][$atomname]['name'] = $atomname; - $info['quicktime'][$atomname]['size'] = $atomsize; - $info['quicktime'][$atomname]['offset'] = $offset; - if (($offset + $atomsize) > $info['avdataend']) { + $info['quicktime'][$atomname]['name'] = $atomname; + $info['quicktime'][$atomname]['size'] = $atomsize; + $info['quicktime'][$atomname]['offset'] = $offset; $this->error('Atom at offset '.$offset.' claims to go beyond end-of-file (length: '.$atomsize.' bytes)'); return false; } - if ($atomsize == 0) { // Furthermore, for historical reasons the list of atoms is optionally // terminated by a 32-bit integer set to 0. If you are writing a program // to read user data atoms, you should allow for the terminating 0. + $info['quicktime'][$atomname]['name'] = $atomname; + $info['quicktime'][$atomname]['size'] = $atomsize; + $info['quicktime'][$atomname]['offset'] = $offset; break; } $atomHierarchy = array(); - $info['quicktime'][$atomname] = $this->QuicktimeParseAtom($atomname, $atomsize, $this->fread(min($atomsize, $atom_data_read_buffer_size)), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); + $parsedAtomData = $this->QuicktimeParseAtom($atomname, $atomsize, $this->fread(min($atomsize - $atom_size_extended_bytes, $atom_data_read_buffer_size)), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); + $parsedAtomData['name'] = $atomname; + $parsedAtomData['size'] = $atomsize; + $parsedAtomData['offset'] = $offset; + if ($atom_size_extended_bytes) { + $parsedAtomData['xsize_bytes'] = $atom_size_extended_bytes; + } + if (in_array($atomname, array('uuid'))) { + @$info['quicktime'][$atomname][] = $parsedAtomData; + } else { + $info['quicktime'][$atomname] = $parsedAtomData; + } $offset += $atomsize; $atomcounter++; @@ -81,7 +114,7 @@ class getid3_quicktime extends getid3_handler unset($info['avdataend_tmp']); } - if (!empty($info['quicktime']['comments']['chapters']) && is_array($info['quicktime']['comments']['chapters']) && (count($info['quicktime']['comments']['chapters']) > 0)) { + if (isset($info['quicktime']['comments']['chapters']) && is_array($info['quicktime']['comments']['chapters']) && (count($info['quicktime']['comments']['chapters']) > 0)) { $durations = $this->quicktime_time_to_sample_table($info); for ($i = 0; $i < count($info['quicktime']['comments']['chapters']); $i++) { $bookmark = array(); @@ -109,59 +142,59 @@ class getid3_quicktime extends getid3_handler if (!empty($info['quicktime']['comments']['location.ISO6709'])) { // https://en.wikipedia.org/wiki/ISO_6709 foreach ($info['quicktime']['comments']['location.ISO6709'] as $ISO6709string) { - $latitude = false; - $longitude = false; - $altitude = false; + $ISO6709parsed = array('latitude'=>false, 'longitude'=>false, 'altitude'=>false); if (preg_match('#^([\\+\\-])([0-9]{2}|[0-9]{4}|[0-9]{6})(\\.[0-9]+)?([\\+\\-])([0-9]{3}|[0-9]{5}|[0-9]{7})(\\.[0-9]+)?(([\\+\\-])([0-9]{3}|[0-9]{5}|[0-9]{7})(\\.[0-9]+)?)?/$#', $ISO6709string, $matches)) { @list($dummy, $lat_sign, $lat_deg, $lat_deg_dec, $lon_sign, $lon_deg, $lon_deg_dec, $dummy, $alt_sign, $alt_deg, $alt_deg_dec) = $matches; if (strlen($lat_deg) == 2) { // [+-]DD.D - $latitude = floatval(ltrim($lat_deg, '0').$lat_deg_dec); + $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim($lat_deg, '0').$lat_deg_dec); } elseif (strlen($lat_deg) == 4) { // [+-]DDMM.M - $latitude = floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval(ltrim(substr($lat_deg, 2, 2), '0').$lat_deg_dec / 60); + $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval(ltrim(substr($lat_deg, 2, 2), '0').$lat_deg_dec / 60); } elseif (strlen($lat_deg) == 6) { // [+-]DDMMSS.S - $latitude = floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval(ltrim(substr($lat_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lat_deg, 4, 2), '0').$lat_deg_dec / 3600); + $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval((int) ltrim(substr($lat_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lat_deg, 4, 2), '0').$lat_deg_dec / 3600); } if (strlen($lon_deg) == 3) { // [+-]DDD.D - $longitude = floatval(ltrim($lon_deg, '0').$lon_deg_dec); + $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim($lon_deg, '0').$lon_deg_dec); } elseif (strlen($lon_deg) == 5) { // [+-]DDDMM.M - $longitude = floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval(ltrim(substr($lon_deg, 2, 2), '0').$lon_deg_dec / 60); + $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval(ltrim(substr($lon_deg, 2, 2), '0').$lon_deg_dec / 60); } elseif (strlen($lon_deg) == 7) { // [+-]DDDMMSS.S - $longitude = floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval(ltrim(substr($lon_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lon_deg, 4, 2), '0').$lon_deg_dec / 3600); + $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval((int) ltrim(substr($lon_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lon_deg, 4, 2), '0').$lon_deg_dec / 3600); } if (strlen($alt_deg) == 3) { // [+-]DDD.D - $altitude = floatval(ltrim($alt_deg, '0').$alt_deg_dec); + $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim($alt_deg, '0').$alt_deg_dec); } elseif (strlen($alt_deg) == 5) { // [+-]DDDMM.M - $altitude = floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval(ltrim(substr($alt_deg, 2, 2), '0').$alt_deg_dec / 60); + $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval(ltrim(substr($alt_deg, 2, 2), '0').$alt_deg_dec / 60); } elseif (strlen($alt_deg) == 7) { // [+-]DDDMMSS.S - $altitude = floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval(ltrim(substr($alt_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($alt_deg, 4, 2), '0').$alt_deg_dec / 3600); + $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval((int) ltrim(substr($alt_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($alt_deg, 4, 2), '0').$alt_deg_dec / 3600); } - if ($latitude !== false) { - $info['quicktime']['comments']['gps_latitude'][] = (($lat_sign == '-') ? -1 : 1) * floatval($latitude); - } - if ($longitude !== false) { - $info['quicktime']['comments']['gps_longitude'][] = (($lon_sign == '-') ? -1 : 1) * floatval($longitude); - } - if ($altitude !== false) { - $info['quicktime']['comments']['gps_altitude'][] = (($alt_sign == '-') ? -1 : 1) * floatval($altitude); + foreach (array('latitude', 'longitude', 'altitude') as $key) { + if ($ISO6709parsed[$key] !== false) { + $value = (($lat_sign == '-') ? -1 : 1) * floatval($ISO6709parsed[$key]); + if (!isset($info['quicktime']['comments']['gps_'.$key]) || !in_array($value, $info['quicktime']['comments']['gps_'.$key])) { + @$info['quicktime']['comments']['gps_'.$key][] = (($lat_sign == '-') ? -1 : 1) * floatval($ISO6709parsed[$key]); + } + } } } - if ($latitude === false) { + if ($ISO6709parsed['latitude'] === false) { $this->warning('location.ISO6709 string not parsed correctly: "'.$ISO6709string.'", please submit as a bug'); } break; } } - if (!isset($info['bitrate']) && isset($info['playtime_seconds'])) { + if (!isset($info['bitrate']) && !empty($info['playtime_seconds'])) { $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; } if (isset($info['bitrate']) && !isset($info['audio']['bitrate']) && !isset($info['quicktime']['video'])) { $info['audio']['bitrate'] = $info['bitrate']; } + if (!empty($info['bitrate']) && !empty($info['audio']['bitrate']) && empty($info['video']['bitrate']) && !empty($info['video']['frame_rate']) && !empty($info['video']['resolution_x']) && ($info['bitrate'] > $info['audio']['bitrate'])) { + $info['video']['bitrate'] = $info['bitrate'] - $info['audio']['bitrate']; + } if (!empty($info['playtime_seconds']) && !isset($info['video']['frame_rate']) && !empty($info['quicktime']['stts_framecount'])) { foreach ($info['quicktime']['stts_framecount'] as $key => $samples_count) { $samples_per_second = $samples_count / $info['playtime_seconds']; @@ -200,1551 +233,1914 @@ class getid3_quicktime extends getid3_handler return true; } + /** + * @param string $atomname + * @param int $atomsize + * @param string $atom_data + * @param int $baseoffset + * @param array $atomHierarchy + * @param bool $ParseAllPossibleAtoms + * + * @return array|false + */ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset, &$atomHierarchy, $ParseAllPossibleAtoms) { // http://developer.apple.com/techpubs/quicktime/qtdevdocs/APIREF/INDEX/atomalphaindex.htm // https://code.google.com/p/mp4v2/wiki/iTunesMetadata $info = &$this->getid3->info; - $atom_parent = end($atomHierarchy); // not array_pop($atomHierarchy); see http://www.getid3.org/phpBB3/viewtopic.php?t=1717 + $atom_parent = end($atomHierarchy); // not array_pop($atomHierarchy); see https://www.getid3.org/phpBB3/viewtopic.php?t=1717 array_push($atomHierarchy, $atomname); + $atom_structure = array(); $atom_structure['hierarchy'] = implode(' ', $atomHierarchy); $atom_structure['name'] = $atomname; $atom_structure['size'] = $atomsize; $atom_structure['offset'] = $baseoffset; - switch ($atomname) { - case 'moov': // MOVie container atom - case 'trak': // TRAcK container atom - case 'clip': // CLIPping container atom - case 'matt': // track MATTe container atom - case 'edts': // EDiTS container atom - case 'tref': // Track REFerence container atom - case 'mdia': // MeDIA container atom - case 'minf': // Media INFormation container atom - case 'dinf': // Data INFormation container atom - case 'udta': // User DaTA container atom - case 'cmov': // Compressed MOVie container atom - case 'rmra': // Reference Movie Record Atom - case 'rmda': // Reference Movie Descriptor Atom - case 'gmhd': // Generic Media info HeaDer atom (seen on QTVR) - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; + if (substr($atomname, 0, 3) == "\x00\x00\x00") { + // https://github.com/JamesHeinrich/getID3/issues/139 + $atomname = getid3_lib::BigEndian2Int($atomname); + $atom_structure['name'] = $atomname; + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + } else { + switch ($atomname) { + case 'moov': // MOVie container atom + case 'moof': // MOvie Fragment box + case 'trak': // TRAcK container atom + case 'traf': // TRAck Fragment box + case 'clip': // CLIPping container atom + case 'matt': // track MATTe container atom + case 'edts': // EDiTS container atom + case 'tref': // Track REFerence container atom + case 'mdia': // MeDIA container atom + case 'minf': // Media INFormation container atom + case 'dinf': // Data INFormation container atom + case 'nmhd': // Null Media HeaDer container atom + case 'udta': // User DaTA container atom + case 'cmov': // Compressed MOVie container atom + case 'rmra': // Reference Movie Record Atom + case 'rmda': // Reference Movie Descriptor Atom + case 'gmhd': // Generic Media info HeaDer atom (seen on QTVR) + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + break; - case 'ilst': // Item LiST container atom - if ($atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms)) { - // some "ilst" atoms contain data atoms that have a numeric name, and the data is far more accessible if the returned array is compacted - $allnumericnames = true; - foreach ($atom_structure['subatoms'] as $subatomarray) { - if (!is_integer($subatomarray['name']) || (count($subatomarray['subatoms']) != 1)) { - $allnumericnames = false; - break; - } - } - if ($allnumericnames) { - $newData = array(); + case 'ilst': // Item LiST container atom + if ($atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms)) { + // some "ilst" atoms contain data atoms that have a numeric name, and the data is far more accessible if the returned array is compacted + $allnumericnames = true; foreach ($atom_structure['subatoms'] as $subatomarray) { - foreach ($subatomarray['subatoms'] as $newData_subatomarray) { - unset($newData_subatomarray['hierarchy'], $newData_subatomarray['name']); - $newData[$subatomarray['name']] = $newData_subatomarray; + if (!is_integer($subatomarray['name']) || (count($subatomarray['subatoms']) != 1)) { + $allnumericnames = false; break; } } - $atom_structure['data'] = $newData; - unset($atom_structure['subatoms']); + if ($allnumericnames) { + $newData = array(); + foreach ($atom_structure['subatoms'] as $subatomarray) { + foreach ($subatomarray['subatoms'] as $newData_subatomarray) { + unset($newData_subatomarray['hierarchy'], $newData_subatomarray['name']); + $newData[$subatomarray['name']] = $newData_subatomarray; + break; + } + } + $atom_structure['data'] = $newData; + unset($atom_structure['subatoms']); + } } - } - break; + break; - case "\x00\x00\x00\x01": - case "\x00\x00\x00\x02": - case "\x00\x00\x00\x03": - case "\x00\x00\x00\x04": - case "\x00\x00\x00\x05": - $atomname = getid3_lib::BigEndian2Int($atomname); - $atom_structure['name'] = $atomname; - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'stbl': // Sample TaBLe container atom - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - $isVideo = false; - $framerate = 0; - $framecount = 0; - foreach ($atom_structure['subatoms'] as $key => $value_array) { - if (isset($value_array['sample_description_table'])) { - foreach ($value_array['sample_description_table'] as $key2 => $value_array2) { - if (isset($value_array2['data_format'])) { - switch ($value_array2['data_format']) { - case 'avc1': - case 'mp4v': - // video data - $isVideo = true; - break; - case 'mp4a': - // audio data - break; + case 'stbl': // Sample TaBLe container atom + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + $isVideo = false; + $framerate = 0; + $framecount = 0; + foreach ($atom_structure['subatoms'] as $key => $value_array) { + if (isset($value_array['sample_description_table'])) { + foreach ($value_array['sample_description_table'] as $key2 => $value_array2) { + if (isset($value_array2['data_format'])) { + switch ($value_array2['data_format']) { + case 'avc1': + case 'mp4v': + // video data + $isVideo = true; + break; + case 'mp4a': + // audio data + break; + } + } + } + } elseif (isset($value_array['time_to_sample_table'])) { + foreach ($value_array['time_to_sample_table'] as $key2 => $value_array2) { + if (isset($value_array2['sample_count']) && isset($value_array2['sample_duration']) && ($value_array2['sample_duration'] > 0)) { + $framerate = round($info['quicktime']['time_scale'] / $value_array2['sample_duration'], 3); + $framecount = $value_array2['sample_count']; } } } - } elseif (isset($value_array['time_to_sample_table'])) { - foreach ($value_array['time_to_sample_table'] as $key2 => $value_array2) { - if (isset($value_array2['sample_count']) && isset($value_array2['sample_duration']) && ($value_array2['sample_duration'] > 0)) { - $framerate = round($info['quicktime']['time_scale'] / $value_array2['sample_duration'], 3); - $framecount = $value_array2['sample_count']; + } + if ($isVideo && $framerate) { + $info['quicktime']['video']['frame_rate'] = $framerate; + $info['video']['frame_rate'] = $info['quicktime']['video']['frame_rate']; + } + if ($isVideo && $framecount) { + $info['quicktime']['video']['frame_count'] = $framecount; + } + break; + + + case "\xA9".'alb': // ALBum + case "\xA9".'ART': // + case "\xA9".'art': // ARTist + case "\xA9".'aut': // + case "\xA9".'cmt': // CoMmenT + case "\xA9".'com': // COMposer + case "\xA9".'cpy': // + case "\xA9".'day': // content created year + case "\xA9".'dir': // + case "\xA9".'ed1': // + case "\xA9".'ed2': // + case "\xA9".'ed3': // + case "\xA9".'ed4': // + case "\xA9".'ed5': // + case "\xA9".'ed6': // + case "\xA9".'ed7': // + case "\xA9".'ed8': // + case "\xA9".'ed9': // + case "\xA9".'enc': // + case "\xA9".'fmt': // + case "\xA9".'gen': // GENre + case "\xA9".'grp': // GRouPing + case "\xA9".'hst': // + case "\xA9".'inf': // + case "\xA9".'lyr': // LYRics + case "\xA9".'mak': // + case "\xA9".'mod': // + case "\xA9".'nam': // full NAMe + case "\xA9".'ope': // + case "\xA9".'PRD': // + case "\xA9".'prf': // + case "\xA9".'req': // + case "\xA9".'src': // + case "\xA9".'swr': // + case "\xA9".'too': // encoder + case "\xA9".'trk': // TRacK + case "\xA9".'url': // + case "\xA9".'wrn': // + case "\xA9".'wrt': // WRiTer + case '----': // itunes specific + case 'aART': // Album ARTist + case 'akID': // iTunes store account type + case 'apID': // Purchase Account + case 'atID': // + case 'catg': // CaTeGory + case 'cmID': // + case 'cnID': // + case 'covr': // COVeR artwork + case 'cpil': // ComPILation + case 'cprt': // CoPyRighT + case 'desc': // DESCription + case 'disk': // DISK number + case 'egid': // Episode Global ID + case 'geID': // + case 'gnre': // GeNRE + case 'hdvd': // HD ViDeo + case 'keyw': // KEYWord + case 'ldes': // Long DEScription + case 'pcst': // PodCaST + case 'pgap': // GAPless Playback + case 'plID': // + case 'purd': // PURchase Date + case 'purl': // Podcast URL + case 'rati': // + case 'rndu': // + case 'rpdu': // + case 'rtng': // RaTiNG + case 'sfID': // iTunes store country + case 'soaa': // SOrt Album Artist + case 'soal': // SOrt ALbum + case 'soar': // SOrt ARtist + case 'soco': // SOrt COmposer + case 'sonm': // SOrt NaMe + case 'sosn': // SOrt Show Name + case 'stik': // + case 'tmpo': // TeMPO (BPM) + case 'trkn': // TRacK Number + case 'tven': // tvEpisodeID + case 'tves': // TV EpiSode + case 'tvnn': // TV Network Name + case 'tvsh': // TV SHow Name + case 'tvsn': // TV SeasoN + if ($atom_parent == 'udta') { + // User data atom handler + $atom_structure['data_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); + $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); + $atom_structure['data'] = substr($atom_data, 4); + + $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); + if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { + $info['comments']['language'][] = $atom_structure['language']; + } + } else { + // Apple item list box atom handler + $atomoffset = 0; + if (substr($atom_data, 2, 2) == "\x10\xB5") { + // not sure what it means, but observed on iPhone4 data. + // Each $atom_data has 2 bytes of datasize, plus 0x10B5, then data + while ($atomoffset < strlen($atom_data)) { + $boxsmallsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 2)); + $boxsmalltype = substr($atom_data, $atomoffset + 2, 2); + $boxsmalldata = substr($atom_data, $atomoffset + 4, $boxsmallsize); + if ($boxsmallsize <= 1) { + $this->warning('Invalid QuickTime atom smallbox size "'.$boxsmallsize.'" in atom "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" at offset: '.($atom_structure['offset'] + $atomoffset)); + $atom_structure['data'] = null; + $atomoffset = strlen($atom_data); + break; + } + switch ($boxsmalltype) { + case "\x10\xB5": + $atom_structure['data'] = $boxsmalldata; + break; + default: + $this->warning('Unknown QuickTime smallbox type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $boxsmalltype).'" ('.trim(getid3_lib::PrintHexBytes($boxsmalltype)).') at offset '.$baseoffset); + $atom_structure['data'] = $atom_data; + break; + } + $atomoffset += (4 + $boxsmallsize); + } + } else { + while ($atomoffset < strlen($atom_data)) { + $boxsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 4)); + $boxtype = substr($atom_data, $atomoffset + 4, 4); + $boxdata = substr($atom_data, $atomoffset + 8, $boxsize - 8); + if ($boxsize <= 1) { + $this->warning('Invalid QuickTime atom box size "'.$boxsize.'" in atom "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" at offset: '.($atom_structure['offset'] + $atomoffset)); + $atom_structure['data'] = null; + $atomoffset = strlen($atom_data); + break; + } + $atomoffset += $boxsize; + + switch ($boxtype) { + case 'mean': + case 'name': + $atom_structure[$boxtype] = substr($boxdata, 4); + break; + + case 'data': + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($boxdata, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($boxdata, 1, 3)); + switch ($atom_structure['flags_raw']) { + case 0: // data flag + case 21: // tmpo/cpil flag + switch ($atomname) { + case 'cpil': + case 'hdvd': + case 'pcst': + case 'pgap': + // 8-bit integer (boolean) + $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); + break; + + case 'tmpo': + // 16-bit integer + $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 2)); + break; + + case 'disk': + case 'trkn': + // binary + $num = getid3_lib::BigEndian2Int(substr($boxdata, 10, 2)); + $num_total = getid3_lib::BigEndian2Int(substr($boxdata, 12, 2)); + $atom_structure['data'] = empty($num) ? '' : $num; + $atom_structure['data'] .= empty($num_total) ? '' : '/'.$num_total; + break; + + case 'gnre': + // enum + $GenreID = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); + $atom_structure['data'] = getid3_id3v1::LookupGenreName($GenreID - 1); + break; + + case 'rtng': + // 8-bit integer + $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); + $atom_structure['data'] = $this->QuicktimeContentRatingLookup($atom_structure[$atomname]); + break; + + case 'stik': + // 8-bit integer (enum) + $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); + $atom_structure['data'] = $this->QuicktimeSTIKLookup($atom_structure[$atomname]); + break; + + case 'sfID': + // 32-bit integer + $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); + $atom_structure['data'] = $this->QuicktimeStoreFrontCodeLookup($atom_structure[$atomname]); + break; + + case 'egid': + case 'purl': + $atom_structure['data'] = substr($boxdata, 8); + break; + + case 'plID': + // 64-bit integer + $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 8)); + break; + + case 'covr': + $atom_structure['data'] = substr($boxdata, 8); + // not a foolproof check, but better than nothing + if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/jpeg'; + } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/png'; + } elseif (preg_match('#^GIF#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/gif'; + } + $info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_structure['data'], 'description'=>'cover'); + break; + + case 'atID': + case 'cnID': + case 'geID': + case 'tves': + case 'tvsn': + default: + // 32-bit integer + $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); + } + break; + + case 1: // text flag + case 13: // image flag + default: + $atom_structure['data'] = substr($boxdata, 8); + if ($atomname == 'covr') { + if (!empty($atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/unknown'; // provide default MIME type to ensure array keys exist + if (function_exists('getimagesizefromstring') && ($getimagesize = getimagesizefromstring($atom_structure['data'])) && !empty($getimagesize['mime'])) { + $atom_structure['image_mime'] = $getimagesize['mime']; + } else { + // if getimagesizefromstring is not available, or fails for some reason, fall back to simple detection of common image formats + $ImageFormatSignatures = array( + 'image/jpeg' => "\xFF\xD8\xFF", + 'image/png' => "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", + 'image/gif' => 'GIF', + ); + foreach ($ImageFormatSignatures as $mime => $image_format_signature) { + if (substr($atom_structure['data'], 0, strlen($image_format_signature)) == $image_format_signature) { + $atom_structure['image_mime'] = $mime; + break; + } + } + } + $info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_structure['data'], 'description'=>'cover'); + } else { + $this->warning('Unknown empty "covr" image at offset '.$baseoffset); + } + } + break; + + } + break; + + default: + $this->warning('Unknown QuickTime box type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $boxtype).'" ('.trim(getid3_lib::PrintHexBytes($boxtype)).') at offset '.$baseoffset); + $atom_structure['data'] = $atom_data; + + } } } } - } - if ($isVideo && $framerate) { - $info['quicktime']['video']['frame_rate'] = $framerate; - $info['video']['frame_rate'] = $info['quicktime']['video']['frame_rate']; - } - if ($isVideo && $framecount) { - $info['quicktime']['video']['frame_count'] = $framecount; - } - break; + $this->CopyToAppropriateCommentsSection($atomname, $atom_structure['data'], $atom_structure['name']); + break; - case "\xA9".'alb': // ALBum - case "\xA9".'ART': // - case "\xA9".'art': // ARTist - case "\xA9".'aut': // - case "\xA9".'cmt': // CoMmenT - case "\xA9".'com': // COMposer - case "\xA9".'cpy': // - case "\xA9".'day': // content created year - case "\xA9".'dir': // - case "\xA9".'ed1': // - case "\xA9".'ed2': // - case "\xA9".'ed3': // - case "\xA9".'ed4': // - case "\xA9".'ed5': // - case "\xA9".'ed6': // - case "\xA9".'ed7': // - case "\xA9".'ed8': // - case "\xA9".'ed9': // - case "\xA9".'enc': // - case "\xA9".'fmt': // - case "\xA9".'gen': // GENre - case "\xA9".'grp': // GRouPing - case "\xA9".'hst': // - case "\xA9".'inf': // - case "\xA9".'lyr': // LYRics - case "\xA9".'mak': // - case "\xA9".'mod': // - case "\xA9".'nam': // full NAMe - case "\xA9".'ope': // - case "\xA9".'PRD': // - case "\xA9".'prf': // - case "\xA9".'req': // - case "\xA9".'src': // - case "\xA9".'swr': // - case "\xA9".'too': // encoder - case "\xA9".'trk': // TRacK - case "\xA9".'url': // - case "\xA9".'wrn': // - case "\xA9".'wrt': // WRiTer - case '----': // itunes specific - case 'aART': // Album ARTist - case 'akID': // iTunes store account type - case 'apID': // Purchase Account - case 'atID': // - case 'catg': // CaTeGory - case 'cmID': // - case 'cnID': // - case 'covr': // COVeR artwork - case 'cpil': // ComPILation - case 'cprt': // CoPyRighT - case 'desc': // DESCription - case 'disk': // DISK number - case 'egid': // Episode Global ID - case 'geID': // - case 'gnre': // GeNRE - case 'hdvd': // HD ViDeo - case 'keyw': // KEYWord - case 'ldes': // Long DEScription - case 'pcst': // PodCaST - case 'pgap': // GAPless Playback - case 'plID': // - case 'purd': // PURchase Date - case 'purl': // Podcast URL - case 'rati': // - case 'rndu': // - case 'rpdu': // - case 'rtng': // RaTiNG - case 'sfID': // iTunes store country - case 'soaa': // SOrt Album Artist - case 'soal': // SOrt ALbum - case 'soar': // SOrt ARtist - case 'soco': // SOrt COmposer - case 'sonm': // SOrt NaMe - case 'sosn': // SOrt Show Name - case 'stik': // - case 'tmpo': // TeMPO (BPM) - case 'trkn': // TRacK Number - case 'tven': // tvEpisodeID - case 'tves': // TV EpiSode - case 'tvnn': // TV Network Name - case 'tvsh': // TV SHow Name - case 'tvsn': // TV SeasoN - if ($atom_parent == 'udta') { - // User data atom handler - $atom_structure['data_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); - $atom_structure['data'] = substr($atom_data, 4); + case 'play': // auto-PLAY atom + $atom_structure['autoplay'] = (bool) getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + + $info['quicktime']['autoplay'] = $atom_structure['autoplay']; + break; + + + case 'WLOC': // Window LOCation atom + $atom_structure['location_x'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); + $atom_structure['location_y'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); + break; + + + case 'LOOP': // LOOPing atom + case 'SelO': // play SELection Only atom + case 'AllF': // play ALL Frames atom + $atom_structure['data'] = getid3_lib::BigEndian2Int($atom_data); + break; + + + case 'name': // + case 'MCPS': // Media Cleaner PRo + case '@PRM': // adobe PReMiere version + case '@PRQ': // adobe PRemiere Quicktime version + $atom_structure['data'] = $atom_data; + break; + + + case 'cmvd': // Compressed MooV Data atom + // Code by ubergeekØubergeek*tv based on information from + // http://developer.apple.com/quicktime/icefloe/dispatch012.html + $atom_structure['unCompressedSize'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); + + $CompressedFileData = substr($atom_data, 4); + if ($UncompressedHeader = @gzuncompress($CompressedFileData)) { + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($UncompressedHeader, 0, $atomHierarchy, $ParseAllPossibleAtoms); + } else { + $this->warning('Error decompressing compressed MOV atom at offset '.$atom_structure['offset']); + } + break; + + + case 'dcom': // Data COMpression atom + $atom_structure['compression_id'] = $atom_data; + $atom_structure['compression_text'] = $this->QuicktimeDCOMLookup($atom_data); + break; + + + case 'rdrf': // Reference movie Data ReFerence atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['flags']['internal_data'] = (bool) ($atom_structure['flags_raw'] & 0x000001); + + $atom_structure['reference_type_name'] = substr($atom_data, 4, 4); + $atom_structure['reference_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + switch ($atom_structure['reference_type_name']) { + case 'url ': + $atom_structure['url'] = $this->NoNullString(substr($atom_data, 12)); + break; + + case 'alis': + $atom_structure['file_alias'] = substr($atom_data, 12); + break; + + case 'rsrc': + $atom_structure['resource_alias'] = substr($atom_data, 12); + break; + + default: + $atom_structure['data'] = substr($atom_data, 12); + break; + } + break; + + + case 'rmqu': // Reference Movie QUality atom + $atom_structure['movie_quality'] = getid3_lib::BigEndian2Int($atom_data); + break; + + + case 'rmcs': // Reference Movie Cpu Speed atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['cpu_speed_rating'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); + break; + + + case 'rmvc': // Reference Movie Version Check atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['gestalt_selector'] = substr($atom_data, 4, 4); + $atom_structure['gestalt_value_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $atom_structure['gestalt_value'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); + $atom_structure['gestalt_check_type'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); + break; + + + case 'rmcd': // Reference Movie Component check atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['component_type'] = substr($atom_data, 4, 4); + $atom_structure['component_subtype'] = substr($atom_data, 8, 4); + $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); + $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); + $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); + $atom_structure['component_min_version'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 4)); + break; + + + case 'rmdr': // Reference Movie Data Rate atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['data_rate'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + + $atom_structure['data_rate_bps'] = $atom_structure['data_rate'] * 10; + break; + + + case 'rmla': // Reference Movie Language Atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { $info['comments']['language'][] = $atom_structure['language']; } - } else { - // Apple item list box atom handler - $atomoffset = 0; - if (substr($atom_data, 2, 2) == "\x10\xB5") { - // not sure what it means, but observed on iPhone4 data. - // Each $atom_data has 2 bytes of datasize, plus 0x10B5, then data - while ($atomoffset < strlen($atom_data)) { - $boxsmallsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 2)); - $boxsmalltype = substr($atom_data, $atomoffset + 2, 2); - $boxsmalldata = substr($atom_data, $atomoffset + 4, $boxsmallsize); - if ($boxsmallsize <= 1) { - $this->warning('Invalid QuickTime atom smallbox size "'.$boxsmallsize.'" in atom "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" at offset: '.($atom_structure['offset'] + $atomoffset)); - $atom_structure['data'] = null; - $atomoffset = strlen($atom_data); - break; - } - switch ($boxsmalltype) { - case "\x10\xB5": - $atom_structure['data'] = $boxsmalldata; - break; - default: - $this->warning('Unknown QuickTime smallbox type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $boxsmalltype).'" ('.trim(getid3_lib::PrintHexBytes($boxsmalltype)).') at offset '.$baseoffset); - $atom_structure['data'] = $atom_data; - break; - } - $atomoffset += (4 + $boxsmallsize); - } + break; + + + case 'ptv ': // Print To Video - defines a movie's full screen mode + // http://developer.apple.com/documentation/QuickTime/APIREF/SOURCESIV/at_ptv-_pg.htm + $atom_structure['display_size_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); + $atom_structure['reserved_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); // hardcoded: 0x0000 + $atom_structure['reserved_2'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x0000 + $atom_structure['slide_show_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 1)); + $atom_structure['play_on_open_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 7, 1)); + + $atom_structure['flags']['play_on_open'] = (bool) $atom_structure['play_on_open_flag']; + $atom_structure['flags']['slide_show'] = (bool) $atom_structure['slide_show_flag']; + + $ptv_lookup = array( + 0 => 'normal', + 1 => 'double', + 2 => 'half', + 3 => 'full', + 4 => 'current' + ); + if (isset($ptv_lookup[$atom_structure['display_size_raw']])) { + $atom_structure['display_size'] = $ptv_lookup[$atom_structure['display_size_raw']]; } else { - while ($atomoffset < strlen($atom_data)) { - $boxsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 4)); - $boxtype = substr($atom_data, $atomoffset + 4, 4); - $boxdata = substr($atom_data, $atomoffset + 8, $boxsize - 8); - if ($boxsize <= 1) { - $this->warning('Invalid QuickTime atom box size "'.$boxsize.'" in atom "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" at offset: '.($atom_structure['offset'] + $atomoffset)); - $atom_structure['data'] = null; - $atomoffset = strlen($atom_data); - break; - } - $atomoffset += $boxsize; + $this->warning('unknown "ptv " display constant ('.$atom_structure['display_size_raw'].')'); + } + break; - switch ($boxtype) { - case 'mean': - case 'name': - $atom_structure[$boxtype] = substr($boxdata, 4); - break; - case 'data': - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($boxdata, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($boxdata, 1, 3)); - switch ($atom_structure['flags_raw']) { - case 0: // data flag - case 21: // tmpo/cpil flag - switch ($atomname) { - case 'cpil': - case 'hdvd': - case 'pcst': - case 'pgap': - // 8-bit integer (boolean) - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - break; + case 'stsd': // Sample Table Sample Description atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - case 'tmpo': - // 16-bit integer - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 2)); - break; - - case 'disk': - case 'trkn': - // binary - $num = getid3_lib::BigEndian2Int(substr($boxdata, 10, 2)); - $num_total = getid3_lib::BigEndian2Int(substr($boxdata, 12, 2)); - $atom_structure['data'] = empty($num) ? '' : $num; - $atom_structure['data'] .= empty($num_total) ? '' : '/'.$num_total; - break; - - case 'gnre': - // enum - $GenreID = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - $atom_structure['data'] = getid3_id3v1::LookupGenreName($GenreID - 1); - break; - - case 'rtng': - // 8-bit integer - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - $atom_structure['data'] = $this->QuicktimeContentRatingLookup($atom_structure[$atomname]); - break; - - case 'stik': - // 8-bit integer (enum) - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - $atom_structure['data'] = $this->QuicktimeSTIKLookup($atom_structure[$atomname]); - break; - - case 'sfID': - // 32-bit integer - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - $atom_structure['data'] = $this->QuicktimeStoreFrontCodeLookup($atom_structure[$atomname]); - break; - - case 'egid': - case 'purl': - $atom_structure['data'] = substr($boxdata, 8); - break; - - case 'plID': - // 64-bit integer - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 8)); - break; - - case 'covr': - $atom_structure['data'] = substr($boxdata, 8); - // not a foolproof check, but better than nothing - if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/jpeg'; - } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/png'; - } elseif (preg_match('#^GIF#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/gif'; - } - break; - - case 'atID': - case 'cnID': - case 'geID': - case 'tves': - case 'tvsn': - default: - // 32-bit integer - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - } - break; - - case 1: // text flag - case 13: // image flag - default: - $atom_structure['data'] = substr($boxdata, 8); - if ($atomname == 'covr') { - // not a foolproof check, but better than nothing - if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/jpeg'; - } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/png'; - } elseif (preg_match('#^GIF#', $atom_structure['data'])) { - $atom_structure['image_mime'] = 'image/gif'; - } - } - break; - - } - break; - - default: - $this->warning('Unknown QuickTime box type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $boxtype).'" ('.trim(getid3_lib::PrintHexBytes($boxtype)).') at offset '.$baseoffset); - $atom_structure['data'] = $atom_data; - - } + // see: https://github.com/JamesHeinrich/getID3/issues/111 + // Some corrupt files have been known to have high bits set in the number_entries field + // This field shouldn't really need to be 32-bits, values stores are likely in the range 1-100000 + // Workaround: mask off the upper byte and throw a warning if it's nonzero + if ($atom_structure['number_entries'] > 0x000FFFFF) { + if ($atom_structure['number_entries'] > 0x00FFFFFF) { + $this->warning('"stsd" atom contains improbably large number_entries (0x'.getid3_lib::PrintHexBytes(substr($atom_data, 4, 4), true, false).' = '.$atom_structure['number_entries'].'), probably in error. Ignoring upper byte and interpreting this as 0x'.getid3_lib::PrintHexBytes(substr($atom_data, 5, 3), true, false).' = '.($atom_structure['number_entries'] & 0x00FFFFFF)); + $atom_structure['number_entries'] = ($atom_structure['number_entries'] & 0x00FFFFFF); + } else { + $this->warning('"stsd" atom contains improbably large number_entries (0x'.getid3_lib::PrintHexBytes(substr($atom_data, 4, 4), true, false).' = '.$atom_structure['number_entries'].'), probably in error. Please report this to info@getid3.org referencing bug report #111'); } } - } - $this->CopyToAppropriateCommentsSection($atomname, $atom_structure['data'], $atom_structure['name']); - break; + $stsdEntriesDataOffset = 8; + for ($i = 0; $i < $atom_structure['number_entries']; $i++) { + $atom_structure['sample_description_table'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 4)); + $stsdEntriesDataOffset += 4; + $atom_structure['sample_description_table'][$i]['data_format'] = substr($atom_data, $stsdEntriesDataOffset, 4); + $stsdEntriesDataOffset += 4; + $atom_structure['sample_description_table'][$i]['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 6)); + $stsdEntriesDataOffset += 6; + $atom_structure['sample_description_table'][$i]['reference_index'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 2)); + $stsdEntriesDataOffset += 2; + $atom_structure['sample_description_table'][$i]['data'] = substr($atom_data, $stsdEntriesDataOffset, ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2)); + $stsdEntriesDataOffset += ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2); - case 'play': // auto-PLAY atom - $atom_structure['autoplay'] = (bool) getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + if (substr($atom_structure['sample_description_table'][$i]['data'], 1, 54) == 'application/octet-stream;type=com.parrot.videometadata') { + // special handling for apparently-malformed (TextMetaDataSampleEntry?) data for some version of Parrot drones + $atom_structure['sample_description_table'][$i]['parrot_frame_metadata']['mime_type'] = substr($atom_structure['sample_description_table'][$i]['data'], 1, 55); + $atom_structure['sample_description_table'][$i]['parrot_frame_metadata']['metadata_version'] = (int) substr($atom_structure['sample_description_table'][$i]['data'], 55, 1); + unset($atom_structure['sample_description_table'][$i]['data']); +$this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in this version of getID3() ['.$this->getid3->version().']'); + continue; + } - $info['quicktime']['autoplay'] = $atom_structure['autoplay']; - break; + $atom_structure['sample_description_table'][$i]['encoder_version'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 0, 2)); + $atom_structure['sample_description_table'][$i]['encoder_revision'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 2, 2)); + $atom_structure['sample_description_table'][$i]['encoder_vendor'] = substr($atom_structure['sample_description_table'][$i]['data'], 4, 4); + switch ($atom_structure['sample_description_table'][$i]['encoder_vendor']) { - case 'WLOC': // Window LOCation atom - $atom_structure['location_x'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['location_y'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); - break; + case "\x00\x00\x00\x00": + // audio tracks + $atom_structure['sample_description_table'][$i]['audio_channels'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 2)); + $atom_structure['sample_description_table'][$i]['audio_bit_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 10, 2)); + $atom_structure['sample_description_table'][$i]['audio_compression_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 2)); + $atom_structure['sample_description_table'][$i]['audio_packet_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 14, 2)); + $atom_structure['sample_description_table'][$i]['audio_sample_rate'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 16, 4)); + // video tracks + // http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap3/qtff3.html + $atom_structure['sample_description_table'][$i]['temporal_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 4)); + $atom_structure['sample_description_table'][$i]['spatial_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 4)); + $atom_structure['sample_description_table'][$i]['width'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 16, 2)); + $atom_structure['sample_description_table'][$i]['height'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 18, 2)); + $atom_structure['sample_description_table'][$i]['resolution_x'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 24, 4)); + $atom_structure['sample_description_table'][$i]['resolution_y'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 28, 4)); + $atom_structure['sample_description_table'][$i]['data_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 32, 4)); + $atom_structure['sample_description_table'][$i]['frame_count'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 36, 2)); + $atom_structure['sample_description_table'][$i]['compressor_name'] = substr($atom_structure['sample_description_table'][$i]['data'], 38, 4); + $atom_structure['sample_description_table'][$i]['pixel_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 42, 2)); + $atom_structure['sample_description_table'][$i]['color_table_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 44, 2)); - case 'LOOP': // LOOPing atom - case 'SelO': // play SELection Only atom - case 'AllF': // play ALL Frames atom - $atom_structure['data'] = getid3_lib::BigEndian2Int($atom_data); - break; + switch ($atom_structure['sample_description_table'][$i]['data_format']) { + case '2vuY': + case 'avc1': + case 'cvid': + case 'dvc ': + case 'dvcp': + case 'gif ': + case 'h263': + case 'hvc1': + case 'jpeg': + case 'kpcd': + case 'mjpa': + case 'mjpb': + case 'mp4v': + case 'png ': + case 'raw ': + case 'rle ': + case 'rpza': + case 'smc ': + case 'SVQ1': + case 'SVQ3': + case 'tiff': + case 'v210': + case 'v216': + case 'v308': + case 'v408': + case 'v410': + case 'yuv2': + $info['fileformat'] = 'mp4'; + $info['video']['fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; + if ($this->QuicktimeVideoCodecLookup($info['video']['fourcc'])) { + $info['video']['fourcc_lookup'] = $this->QuicktimeVideoCodecLookup($info['video']['fourcc']); + } + // https://www.getid3.org/phpBB3/viewtopic.php?t=1550 + //if ((!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($atom_structure['sample_description_table'][$i]['width'])) && (empty($info['video']['resolution_x']) || empty($info['video']['resolution_y']) || (number_format($info['video']['resolution_x'], 6) != number_format(round($info['video']['resolution_x']), 6)) || (number_format($info['video']['resolution_y'], 6) != number_format(round($info['video']['resolution_y']), 6)))) { // ugly check for floating point numbers + if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($atom_structure['sample_description_table'][$i]['height'])) { + // assume that values stored here are more important than values stored in [tkhd] atom + $info['video']['resolution_x'] = $atom_structure['sample_description_table'][$i]['width']; + $info['video']['resolution_y'] = $atom_structure['sample_description_table'][$i]['height']; + $info['quicktime']['video']['resolution_x'] = $info['video']['resolution_x']; + $info['quicktime']['video']['resolution_y'] = $info['video']['resolution_y']; + } + break; - case 'name': // - case 'MCPS': // Media Cleaner PRo - case '@PRM': // adobe PReMiere version - case '@PRQ': // adobe PRemiere Quicktime version - $atom_structure['data'] = $atom_data; - break; + case 'qtvr': + $info['video']['dataformat'] = 'quicktimevr'; + break; + case 'mp4a': + default: + $info['quicktime']['audio']['codec'] = $this->QuicktimeAudioCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); + $info['quicktime']['audio']['sample_rate'] = $atom_structure['sample_description_table'][$i]['audio_sample_rate']; + $info['quicktime']['audio']['channels'] = $atom_structure['sample_description_table'][$i]['audio_channels']; + $info['quicktime']['audio']['bit_depth'] = $atom_structure['sample_description_table'][$i]['audio_bit_depth']; + $info['audio']['codec'] = $info['quicktime']['audio']['codec']; + $info['audio']['sample_rate'] = $info['quicktime']['audio']['sample_rate']; + $info['audio']['channels'] = $info['quicktime']['audio']['channels']; + $info['audio']['bits_per_sample'] = $info['quicktime']['audio']['bit_depth']; + switch ($atom_structure['sample_description_table'][$i]['data_format']) { + case 'raw ': // PCM + case 'alac': // Apple Lossless Audio Codec + case 'sowt': // signed/two's complement (Little Endian) + case 'twos': // signed/two's complement (Big Endian) + case 'in24': // 24-bit Integer + case 'in32': // 32-bit Integer + case 'fl32': // 32-bit Floating Point + case 'fl64': // 64-bit Floating Point + $info['audio']['lossless'] = $info['quicktime']['audio']['lossless'] = true; + $info['audio']['bitrate'] = $info['quicktime']['audio']['bitrate'] = $info['audio']['channels'] * $info['audio']['bits_per_sample'] * $info['audio']['sample_rate']; + break; + default: + $info['audio']['lossless'] = false; + break; + } + break; + } + break; - case 'cmvd': // Compressed MooV Data atom - // Code by ubergeekØubergeek*tv based on information from - // http://developer.apple.com/quicktime/icefloe/dispatch012.html - $atom_structure['unCompressedSize'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); + default: + switch ($atom_structure['sample_description_table'][$i]['data_format']) { + case 'mp4s': + $info['fileformat'] = 'mp4'; + break; - $CompressedFileData = substr($atom_data, 4); - if ($UncompressedHeader = @gzuncompress($CompressedFileData)) { - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($UncompressedHeader, 0, $atomHierarchy, $ParseAllPossibleAtoms); - } else { - $this->warning('Error decompressing compressed MOV atom at offset '.$atom_structure['offset']); - } - break; + default: + // video atom + $atom_structure['sample_description_table'][$i]['video_temporal_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 4)); + $atom_structure['sample_description_table'][$i]['video_spatial_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 4)); + $atom_structure['sample_description_table'][$i]['video_frame_width'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 16, 2)); + $atom_structure['sample_description_table'][$i]['video_frame_height'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 18, 2)); + $atom_structure['sample_description_table'][$i]['video_resolution_x'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 20, 4)); + $atom_structure['sample_description_table'][$i]['video_resolution_y'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 24, 4)); + $atom_structure['sample_description_table'][$i]['video_data_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 28, 4)); + $atom_structure['sample_description_table'][$i]['video_frame_count'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 32, 2)); + $atom_structure['sample_description_table'][$i]['video_encoder_name_len'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 34, 1)); + $atom_structure['sample_description_table'][$i]['video_encoder_name'] = substr($atom_structure['sample_description_table'][$i]['data'], 35, $atom_structure['sample_description_table'][$i]['video_encoder_name_len']); + $atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 66, 2)); + $atom_structure['sample_description_table'][$i]['video_color_table_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 68, 2)); + $atom_structure['sample_description_table'][$i]['video_pixel_color_type'] = (((int) $atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] > 32) ? 'grayscale' : 'color'); + $atom_structure['sample_description_table'][$i]['video_pixel_color_name'] = $this->QuicktimeColorNameLookup($atom_structure['sample_description_table'][$i]['video_pixel_color_depth']); - case 'dcom': // Data COMpression atom - $atom_structure['compression_id'] = $atom_data; - $atom_structure['compression_text'] = $this->QuicktimeDCOMLookup($atom_data); - break; + if ($atom_structure['sample_description_table'][$i]['video_pixel_color_name'] != 'invalid') { + $info['quicktime']['video']['codec_fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; + $info['quicktime']['video']['codec_fourcc_lookup'] = $this->QuicktimeVideoCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); + $info['quicktime']['video']['codec'] = (((int) $atom_structure['sample_description_table'][$i]['video_encoder_name_len'] > 0) ? $atom_structure['sample_description_table'][$i]['video_encoder_name'] : $atom_structure['sample_description_table'][$i]['data_format']); + $info['quicktime']['video']['color_depth'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_depth']; + $info['quicktime']['video']['color_depth_name'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_name']; + $info['video']['codec'] = $info['quicktime']['video']['codec']; + $info['video']['bits_per_sample'] = $info['quicktime']['video']['color_depth']; + } + $info['video']['lossless'] = false; + $info['video']['pixel_aspect_ratio'] = (float) 1; + break; + } + break; + } + switch (strtolower($atom_structure['sample_description_table'][$i]['data_format'])) { + case 'mp4a': + $info['audio']['dataformat'] = 'mp4'; + $info['quicktime']['audio']['codec'] = 'mp4'; + break; - case 'rdrf': // Reference movie Data ReFerence atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['flags']['internal_data'] = (bool) ($atom_structure['flags_raw'] & 0x000001); + case '3ivx': + case '3iv1': + case '3iv2': + $info['video']['dataformat'] = '3ivx'; + break; - $atom_structure['reference_type_name'] = substr($atom_data, 4, 4); - $atom_structure['reference_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - switch ($atom_structure['reference_type_name']) { - case 'url ': - $atom_structure['url'] = $this->NoNullString(substr($atom_data, 12)); - break; + case 'xvid': + $info['video']['dataformat'] = 'xvid'; + break; - case 'alis': - $atom_structure['file_alias'] = substr($atom_data, 12); - break; + case 'mp4v': + $info['video']['dataformat'] = 'mpeg4'; + break; - case 'rsrc': - $atom_structure['resource_alias'] = substr($atom_data, 12); - break; + case 'divx': + case 'div1': + case 'div2': + case 'div3': + case 'div4': + case 'div5': + case 'div6': + $info['video']['dataformat'] = 'divx'; + break; - default: - $atom_structure['data'] = substr($atom_data, 12); - break; - } - break; - - - case 'rmqu': // Reference Movie QUality atom - $atom_structure['movie_quality'] = getid3_lib::BigEndian2Int($atom_data); - break; - - - case 'rmcs': // Reference Movie Cpu Speed atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['cpu_speed_rating'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - break; - - - case 'rmvc': // Reference Movie Version Check atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['gestalt_selector'] = substr($atom_data, 4, 4); - $atom_structure['gestalt_value_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['gestalt_value'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['gestalt_check_type'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); - break; - - - case 'rmcd': // Reference Movie Component check atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['component_type'] = substr($atom_data, 4, 4); - $atom_structure['component_subtype'] = substr($atom_data, 8, 4); - $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); - $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['component_min_version'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 4)); - break; - - - case 'rmdr': // Reference Movie Data Rate atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['data_rate'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - - $atom_structure['data_rate_bps'] = $atom_structure['data_rate'] * 10; - break; - - - case 'rmla': // Reference Movie Language Atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - - $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); - if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { - $info['comments']['language'][] = $atom_structure['language']; - } - break; - - - case 'rmla': // Reference Movie Language Atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - break; - - - case 'ptv ': // Print To Video - defines a movie's full screen mode - // http://developer.apple.com/documentation/QuickTime/APIREF/SOURCESIV/at_ptv-_pg.htm - $atom_structure['display_size_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['reserved_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); // hardcoded: 0x0000 - $atom_structure['reserved_2'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x0000 - $atom_structure['slide_show_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 1)); - $atom_structure['play_on_open_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 7, 1)); - - $atom_structure['flags']['play_on_open'] = (bool) $atom_structure['play_on_open_flag']; - $atom_structure['flags']['slide_show'] = (bool) $atom_structure['slide_show_flag']; - - $ptv_lookup[0] = 'normal'; - $ptv_lookup[1] = 'double'; - $ptv_lookup[2] = 'half'; - $ptv_lookup[3] = 'full'; - $ptv_lookup[4] = 'current'; - if (isset($ptv_lookup[$atom_structure['display_size_raw']])) { - $atom_structure['display_size'] = $ptv_lookup[$atom_structure['display_size_raw']]; - } else { - $this->warning('unknown "ptv " display constant ('.$atom_structure['display_size_raw'].')'); - } - break; - - - case 'stsd': // Sample Table Sample Description atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - - // see: https://github.com/JamesHeinrich/getID3/issues/111 - // Some corrupt files have been known to have high bits set in the number_entries field - // This field shouldn't really need to be 32-bits, values stores are likely in the range 1-100000 - // Workaround: mask off the upper byte and throw a warning if it's nonzero - if ($atom_structure['number_entries'] > 0x000FFFFF) { - if ($atom_structure['number_entries'] > 0x00FFFFFF) { - $this->warning('"stsd" atom contains improbably large number_entries (0x'.getid3_lib::PrintHexBytes(substr($atom_data, 4, 4), true, false).' = '.$atom_structure['number_entries'].'), probably in error. Ignoring upper byte and interpreting this as 0x'.getid3_lib::PrintHexBytes(substr($atom_data, 5, 3), true, false).' = '.($atom_structure['number_entries'] & 0x00FFFFFF)); - $atom_structure['number_entries'] = ($atom_structure['number_entries'] & 0x00FFFFFF); - } else { - $this->warning('"stsd" atom contains improbably large number_entries (0x'.getid3_lib::PrintHexBytes(substr($atom_data, 4, 4), true, false).' = '.$atom_structure['number_entries'].'), probably in error. Please report this to info@getid3.org referencing bug report #111'); + default: + // do nothing + break; + } + unset($atom_structure['sample_description_table'][$i]['data']); } - } + break; - $stsdEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_description_table'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 4)); - $stsdEntriesDataOffset += 4; - $atom_structure['sample_description_table'][$i]['data_format'] = substr($atom_data, $stsdEntriesDataOffset, 4); - $stsdEntriesDataOffset += 4; - $atom_structure['sample_description_table'][$i]['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 6)); - $stsdEntriesDataOffset += 6; - $atom_structure['sample_description_table'][$i]['reference_index'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 2)); - $stsdEntriesDataOffset += 2; - $atom_structure['sample_description_table'][$i]['data'] = substr($atom_data, $stsdEntriesDataOffset, ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2)); - $stsdEntriesDataOffset += ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2); - $atom_structure['sample_description_table'][$i]['encoder_version'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 0, 2)); - $atom_structure['sample_description_table'][$i]['encoder_revision'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 2, 2)); - $atom_structure['sample_description_table'][$i]['encoder_vendor'] = substr($atom_structure['sample_description_table'][$i]['data'], 4, 4); + case 'stts': // Sample Table Time-to-Sample atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $sttsEntriesDataOffset = 8; + //$FrameRateCalculatorArray = array(); + $frames_count = 0; - switch ($atom_structure['sample_description_table'][$i]['encoder_vendor']) { - - case "\x00\x00\x00\x00": - // audio tracks - $atom_structure['sample_description_table'][$i]['audio_channels'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 2)); - $atom_structure['sample_description_table'][$i]['audio_bit_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 10, 2)); - $atom_structure['sample_description_table'][$i]['audio_compression_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 2)); - $atom_structure['sample_description_table'][$i]['audio_packet_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 14, 2)); - $atom_structure['sample_description_table'][$i]['audio_sample_rate'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 16, 4)); - - // video tracks - // http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap3/qtff3.html - $atom_structure['sample_description_table'][$i]['temporal_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 4)); - $atom_structure['sample_description_table'][$i]['spatial_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 4)); - $atom_structure['sample_description_table'][$i]['width'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 16, 2)); - $atom_structure['sample_description_table'][$i]['height'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 18, 2)); - $atom_structure['sample_description_table'][$i]['resolution_x'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 24, 4)); - $atom_structure['sample_description_table'][$i]['resolution_y'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 28, 4)); - $atom_structure['sample_description_table'][$i]['data_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 32, 4)); - $atom_structure['sample_description_table'][$i]['frame_count'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 36, 2)); - $atom_structure['sample_description_table'][$i]['compressor_name'] = substr($atom_structure['sample_description_table'][$i]['data'], 38, 4); - $atom_structure['sample_description_table'][$i]['pixel_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 42, 2)); - $atom_structure['sample_description_table'][$i]['color_table_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 44, 2)); - - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case '2vuY': - case 'avc1': - case 'cvid': - case 'dvc ': - case 'dvcp': - case 'gif ': - case 'h263': - case 'jpeg': - case 'kpcd': - case 'mjpa': - case 'mjpb': - case 'mp4v': - case 'png ': - case 'raw ': - case 'rle ': - case 'rpza': - case 'smc ': - case 'SVQ1': - case 'SVQ3': - case 'tiff': - case 'v210': - case 'v216': - case 'v308': - case 'v408': - case 'v410': - case 'yuv2': - $info['fileformat'] = 'mp4'; - $info['video']['fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; -// http://www.getid3.org/phpBB3/viewtopic.php?t=1550 -//if ((!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($atom_structure['sample_description_table'][$i]['width'])) && (empty($info['video']['resolution_x']) || empty($info['video']['resolution_y']) || (number_format($info['video']['resolution_x'], 6) != number_format(round($info['video']['resolution_x']), 6)) || (number_format($info['video']['resolution_y'], 6) != number_format(round($info['video']['resolution_y']), 6)))) { // ugly check for floating point numbers -if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($atom_structure['sample_description_table'][$i]['height'])) { - // assume that values stored here are more important than values stored in [tkhd] atom - $info['video']['resolution_x'] = $atom_structure['sample_description_table'][$i]['width']; - $info['video']['resolution_y'] = $atom_structure['sample_description_table'][$i]['height']; - $info['quicktime']['video']['resolution_x'] = $info['video']['resolution_x']; - $info['quicktime']['video']['resolution_y'] = $info['video']['resolution_y']; -} - break; - - case 'qtvr': - $info['video']['dataformat'] = 'quicktimevr'; - break; - - case 'mp4a': - default: - $info['quicktime']['audio']['codec'] = $this->QuicktimeAudioCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['audio']['sample_rate'] = $atom_structure['sample_description_table'][$i]['audio_sample_rate']; - $info['quicktime']['audio']['channels'] = $atom_structure['sample_description_table'][$i]['audio_channels']; - $info['quicktime']['audio']['bit_depth'] = $atom_structure['sample_description_table'][$i]['audio_bit_depth']; - $info['audio']['codec'] = $info['quicktime']['audio']['codec']; - $info['audio']['sample_rate'] = $info['quicktime']['audio']['sample_rate']; - $info['audio']['channels'] = $info['quicktime']['audio']['channels']; - $info['audio']['bits_per_sample'] = $info['quicktime']['audio']['bit_depth']; - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case 'raw ': // PCM - case 'alac': // Apple Lossless Audio Codec - $info['audio']['lossless'] = true; - break; - default: - $info['audio']['lossless'] = false; - break; - } - break; - } - break; - - default: - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case 'mp4s': - $info['fileformat'] = 'mp4'; - break; - - default: - // video atom - $atom_structure['sample_description_table'][$i]['video_temporal_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 4)); - $atom_structure['sample_description_table'][$i]['video_spatial_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 4)); - $atom_structure['sample_description_table'][$i]['video_frame_width'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 16, 2)); - $atom_structure['sample_description_table'][$i]['video_frame_height'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 18, 2)); - $atom_structure['sample_description_table'][$i]['video_resolution_x'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 20, 4)); - $atom_structure['sample_description_table'][$i]['video_resolution_y'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 24, 4)); - $atom_structure['sample_description_table'][$i]['video_data_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 28, 4)); - $atom_structure['sample_description_table'][$i]['video_frame_count'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 32, 2)); - $atom_structure['sample_description_table'][$i]['video_encoder_name_len'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 34, 1)); - $atom_structure['sample_description_table'][$i]['video_encoder_name'] = substr($atom_structure['sample_description_table'][$i]['data'], 35, $atom_structure['sample_description_table'][$i]['video_encoder_name_len']); - $atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 66, 2)); - $atom_structure['sample_description_table'][$i]['video_color_table_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 68, 2)); - - $atom_structure['sample_description_table'][$i]['video_pixel_color_type'] = (($atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] > 32) ? 'grayscale' : 'color'); - $atom_structure['sample_description_table'][$i]['video_pixel_color_name'] = $this->QuicktimeColorNameLookup($atom_structure['sample_description_table'][$i]['video_pixel_color_depth']); - - if ($atom_structure['sample_description_table'][$i]['video_pixel_color_name'] != 'invalid') { - $info['quicktime']['video']['codec_fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; - $info['quicktime']['video']['codec_fourcc_lookup'] = $this->QuicktimeVideoCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['video']['codec'] = (($atom_structure['sample_description_table'][$i]['video_encoder_name_len'] > 0) ? $atom_structure['sample_description_table'][$i]['video_encoder_name'] : $atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['video']['color_depth'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_depth']; - $info['quicktime']['video']['color_depth_name'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_name']; - - $info['video']['codec'] = $info['quicktime']['video']['codec']; - $info['video']['bits_per_sample'] = $info['quicktime']['video']['color_depth']; - } - $info['video']['lossless'] = false; - $info['video']['pixel_aspect_ratio'] = (float) 1; - break; - } - break; + $max_stts_entries_to_scan = ($info['php_memory_limit'] ? min(floor($this->getid3->memory_limit / 10000), $atom_structure['number_entries']) : $atom_structure['number_entries']); + if ($max_stts_entries_to_scan < $atom_structure['number_entries']) { + $this->warning('QuickTime atom "stts" has '.$atom_structure['number_entries'].' but only scanning the first '.$max_stts_entries_to_scan.' entries due to limited PHP memory available ('.floor($this->getid3->memory_limit / 1048576).'MB).'); } - switch (strtolower($atom_structure['sample_description_table'][$i]['data_format'])) { - case 'mp4a': - $info['audio']['dataformat'] = 'mp4'; - $info['quicktime']['audio']['codec'] = 'mp4'; - break; + for ($i = 0; $i < $max_stts_entries_to_scan; $i++) { + $atom_structure['time_to_sample_table'][$i]['sample_count'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); + $sttsEntriesDataOffset += 4; + $atom_structure['time_to_sample_table'][$i]['sample_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); + $sttsEntriesDataOffset += 4; - case '3ivx': - case '3iv1': - case '3iv2': - $info['video']['dataformat'] = '3ivx'; - break; + $frames_count += $atom_structure['time_to_sample_table'][$i]['sample_count']; - case 'xvid': - $info['video']['dataformat'] = 'xvid'; - break; - - case 'mp4v': - $info['video']['dataformat'] = 'mpeg4'; - break; - - case 'divx': - case 'div1': - case 'div2': - case 'div3': - case 'div4': - case 'div5': - case 'div6': - $info['video']['dataformat'] = 'divx'; - break; - - default: - // do nothing - break; + // THIS SECTION REPLACED WITH CODE IN "stbl" ATOM + //if (!empty($info['quicktime']['time_scale']) && ($atom_structure['time_to_sample_table'][$i]['sample_duration'] > 0)) { + // $stts_new_framerate = $info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration']; + // if ($stts_new_framerate <= 60) { + // // some atoms have durations of "1" giving a very large framerate, which probably is not right + // $info['video']['frame_rate'] = max($info['video']['frame_rate'], $stts_new_framerate); + // } + //} + // + //$FrameRateCalculatorArray[($info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration'])] += $atom_structure['time_to_sample_table'][$i]['sample_count']; } - unset($atom_structure['sample_description_table'][$i]['data']); - } - break; - - - case 'stts': // Sample Table Time-to-Sample atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $sttsEntriesDataOffset = 8; - //$FrameRateCalculatorArray = array(); - $frames_count = 0; - - $max_stts_entries_to_scan = ($info['php_memory_limit'] ? min(floor($this->getid3->memory_limit / 10000), $atom_structure['number_entries']) : $atom_structure['number_entries']); - if ($max_stts_entries_to_scan < $atom_structure['number_entries']) { - $this->warning('QuickTime atom "stts" has '.$atom_structure['number_entries'].' but only scanning the first '.$max_stts_entries_to_scan.' entries due to limited PHP memory available ('.floor($atom_structure['number_entries'] / 1048576).'MB).'); - } - for ($i = 0; $i < $max_stts_entries_to_scan; $i++) { - $atom_structure['time_to_sample_table'][$i]['sample_count'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); - $sttsEntriesDataOffset += 4; - $atom_structure['time_to_sample_table'][$i]['sample_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); - $sttsEntriesDataOffset += 4; - - $frames_count += $atom_structure['time_to_sample_table'][$i]['sample_count']; - - // THIS SECTION REPLACED WITH CODE IN "stbl" ATOM - //if (!empty($info['quicktime']['time_scale']) && ($atom_structure['time_to_sample_table'][$i]['sample_duration'] > 0)) { - // $stts_new_framerate = $info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration']; - // if ($stts_new_framerate <= 60) { - // // some atoms have durations of "1" giving a very large framerate, which probably is not right - // $info['video']['frame_rate'] = max($info['video']['frame_rate'], $stts_new_framerate); + $info['quicktime']['stts_framecount'][] = $frames_count; + //$sttsFramesTotal = 0; + //$sttsSecondsTotal = 0; + //foreach ($FrameRateCalculatorArray as $frames_per_second => $frame_count) { + // if (($frames_per_second > 60) || ($frames_per_second < 1)) { + // // not video FPS information, probably audio information + // $sttsFramesTotal = 0; + // $sttsSecondsTotal = 0; + // break; + // } + // $sttsFramesTotal += $frame_count; + // $sttsSecondsTotal += $frame_count / $frames_per_second; + //} + //if (($sttsFramesTotal > 0) && ($sttsSecondsTotal > 0)) { + // if (($sttsFramesTotal / $sttsSecondsTotal) > $info['video']['frame_rate']) { + // $info['video']['frame_rate'] = $sttsFramesTotal / $sttsSecondsTotal; // } //} - // - //$FrameRateCalculatorArray[($info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration'])] += $atom_structure['time_to_sample_table'][$i]['sample_count']; - } - $info['quicktime']['stts_framecount'][] = $frames_count; - //$sttsFramesTotal = 0; - //$sttsSecondsTotal = 0; - //foreach ($FrameRateCalculatorArray as $frames_per_second => $frame_count) { - // if (($frames_per_second > 60) || ($frames_per_second < 1)) { - // // not video FPS information, probably audio information - // $sttsFramesTotal = 0; - // $sttsSecondsTotal = 0; - // break; - // } - // $sttsFramesTotal += $frame_count; - // $sttsSecondsTotal += $frame_count / $frames_per_second; - //} - //if (($sttsFramesTotal > 0) && ($sttsSecondsTotal > 0)) { - // if (($sttsFramesTotal / $sttsSecondsTotal) > $info['video']['frame_rate']) { - // $info['video']['frame_rate'] = $sttsFramesTotal / $sttsSecondsTotal; - // } - //} - break; + break; - case 'stss': // Sample Table Sync Sample (key frames) atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stssEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['time_to_sample_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stssEntriesDataOffset, 4)); - $stssEntriesDataOffset += 4; - } - } - break; - - - case 'stsc': // Sample Table Sample-to-Chunk atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stscEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_to_chunk_table'][$i]['first_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - $atom_structure['sample_to_chunk_table'][$i]['samples_per_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - $atom_structure['sample_to_chunk_table'][$i]['sample_description'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - } - } - break; - - - case 'stsz': // Sample Table SiZe atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['sample_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $stszEntriesDataOffset = 12; - if ($atom_structure['sample_size'] == 0) { + case 'stss': // Sample Table Sync Sample (key frames) atom + if ($ParseAllPossibleAtoms) { + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $stssEntriesDataOffset = 8; for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_size_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stszEntriesDataOffset, 4)); - $stszEntriesDataOffset += 4; + $atom_structure['time_to_sample_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stssEntriesDataOffset, 4)); + $stssEntriesDataOffset += 4; } } - } - break; + break; - case 'stco': // Sample Table Chunk Offset atom - if ($ParseAllPossibleAtoms) { + case 'stsc': // Sample Table Sample-to-Chunk atom + if ($ParseAllPossibleAtoms) { + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $stscEntriesDataOffset = 8; + for ($i = 0; $i < $atom_structure['number_entries']; $i++) { + $atom_structure['sample_to_chunk_table'][$i]['first_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); + $stscEntriesDataOffset += 4; + $atom_structure['sample_to_chunk_table'][$i]['samples_per_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); + $stscEntriesDataOffset += 4; + $atom_structure['sample_to_chunk_table'][$i]['sample_description'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); + $stscEntriesDataOffset += 4; + } + } + break; + + + case 'stsz': // Sample Table SiZe atom + if ($ParseAllPossibleAtoms) { + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['sample_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $stszEntriesDataOffset = 12; + if ($atom_structure['sample_size'] == 0) { + for ($i = 0; $i < $atom_structure['number_entries']; $i++) { + $atom_structure['sample_size_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stszEntriesDataOffset, 4)); + $stszEntriesDataOffset += 4; + } + } + } + break; + + + case 'stco': // Sample Table Chunk Offset atom +// if (true) { + if ($ParseAllPossibleAtoms) { + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $stcoEntriesDataOffset = 8; + for ($i = 0; $i < $atom_structure['number_entries']; $i++) { + $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 4)); + $stcoEntriesDataOffset += 4; + } + } + break; + + + case 'co64': // Chunk Offset 64-bit (version of "stco" that supports > 2GB files) + if ($ParseAllPossibleAtoms) { + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $stcoEntriesDataOffset = 8; + for ($i = 0; $i < $atom_structure['number_entries']; $i++) { + $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 8)); + $stcoEntriesDataOffset += 8; + } + } + break; + + + case 'dref': // Data REFerence atom $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stcoEntriesDataOffset = 8; + $drefDataOffset = 8; for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 4)); - $stcoEntriesDataOffset += 4; + $atom_structure['data_references'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 4)); + $drefDataOffset += 4; + $atom_structure['data_references'][$i]['type'] = substr($atom_data, $drefDataOffset, 4); + $drefDataOffset += 4; + $atom_structure['data_references'][$i]['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 1)); + $drefDataOffset += 1; + $atom_structure['data_references'][$i]['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 3)); // hardcoded: 0x0000 + $drefDataOffset += 3; + $atom_structure['data_references'][$i]['data'] = substr($atom_data, $drefDataOffset, ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3)); + $drefDataOffset += ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3); + + $atom_structure['data_references'][$i]['flags']['self_reference'] = (bool) ($atom_structure['data_references'][$i]['flags_raw'] & 0x001); } - } - break; + break; - case 'co64': // Chunk Offset 64-bit (version of "stco" that supports > 2GB files) - if ($ParseAllPossibleAtoms) { + case 'gmin': // base Media INformation atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); + $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); + $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); + $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); + $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 2)); + $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); + break; + + + case 'smhd': // Sound Media information HeaDer atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); + $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); + break; + + + case 'vmhd': // Video Media information HeaDer atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); + $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); + $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); + $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); + + $atom_structure['flags']['no_lean_ahead'] = (bool) ($atom_structure['flags_raw'] & 0x001); + break; + + + case 'hdlr': // HanDLeR reference atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['component_type'] = substr($atom_data, 4, 4); + $atom_structure['component_subtype'] = substr($atom_data, 8, 4); + $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); + $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); + $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); + $atom_structure['component_name'] = $this->MaybePascal2String(substr($atom_data, 24)); + + if (($atom_structure['component_subtype'] == 'STpn') && ($atom_structure['component_manufacturer'] == 'zzzz')) { + $info['video']['dataformat'] = 'quicktimevr'; + } + break; + + + case 'mdhd': // MeDia HeaDer atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); + $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); + $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 2)); + $atom_structure['quality'] = getid3_lib::BigEndian2Int(substr($atom_data, 22, 2)); + + if ($atom_structure['time_scale'] == 0) { + $this->error('Corrupt Quicktime file: mdhd.time_scale == zero'); + return false; + } + $info['quicktime']['time_scale'] = ((isset($info['quicktime']['time_scale']) && ($info['quicktime']['time_scale'] < 1000)) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); + + $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); + $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); + $atom_structure['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; + $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); + if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { + $info['comments']['language'][] = $atom_structure['language']; + } + $info['quicktime']['timestamps_unix']['create'][$atom_structure['hierarchy']] = $atom_structure['creation_time_unix']; + $info['quicktime']['timestamps_unix']['modify'][$atom_structure['hierarchy']] = $atom_structure['modify_time_unix']; + break; + + + case 'pnot': // Preview atom + $atom_structure['modification_date'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // "standard Macintosh format" + $atom_structure['version_number'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x00 + $atom_structure['atom_type'] = substr($atom_data, 6, 4); // usually: 'PICT' + $atom_structure['atom_index'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); // usually: 0x01 + + $atom_structure['modification_date_unix'] = getid3_lib::DateMac2Unix($atom_structure['modification_date']); + $info['quicktime']['timestamps_unix']['modify'][$atom_structure['hierarchy']] = $atom_structure['modification_date_unix']; + break; + + + case 'crgn': // Clipping ReGioN atom + $atom_structure['region_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); // The Region size, Region boundary box, + $atom_structure['boundary_box'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 8)); // and Clipping region data fields + $atom_structure['clipping_data'] = substr($atom_data, 10); // constitute a QuickDraw region. + break; + + + case 'load': // track LOAD settings atom + $atom_structure['preload_start_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); + $atom_structure['preload_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['preload_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $atom_structure['default_hints_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); + + $atom_structure['default_hints']['double_buffer'] = (bool) ($atom_structure['default_hints_raw'] & 0x0020); + $atom_structure['default_hints']['high_quality'] = (bool) ($atom_structure['default_hints_raw'] & 0x0100); + break; + + + case 'tmcd': // TiMe CoDe atom + case 'chap': // CHAPter list atom + case 'sync': // SYNChronization atom + case 'scpt': // tranSCriPT atom + case 'ssrc': // non-primary SouRCe atom + for ($i = 0; $i < strlen($atom_data); $i += 4) { + @$atom_structure['track_id'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4)); + } + break; + + + case 'elst': // Edit LiST atom $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stcoEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 8)); - $stcoEntriesDataOffset += 8; + for ($i = 0; $i < $atom_structure['number_entries']; $i++ ) { + $atom_structure['edit_list'][$i]['track_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 0, 4)); + $atom_structure['edit_list'][$i]['media_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 4, 4)); + $atom_structure['edit_list'][$i]['media_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 8 + ($i * 12) + 8, 4)); } - } - break; + break; - case 'dref': // Data REFerence atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $drefDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['data_references'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 4)); - $drefDataOffset += 4; - $atom_structure['data_references'][$i]['type'] = substr($atom_data, $drefDataOffset, 4); - $drefDataOffset += 4; - $atom_structure['data_references'][$i]['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 1)); - $drefDataOffset += 1; - $atom_structure['data_references'][$i]['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 3)); // hardcoded: 0x0000 - $drefDataOffset += 3; - $atom_structure['data_references'][$i]['data'] = substr($atom_data, $drefDataOffset, ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3)); - $drefDataOffset += ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3); - - $atom_structure['data_references'][$i]['flags']['self_reference'] = (bool) ($atom_structure['data_references'][$i]['flags_raw'] & 0x001); - } - break; + case 'kmat': // compressed MATte atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['matte_data_raw'] = substr($atom_data, 4); + break; - case 'gmin': // base Media INformation atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); - $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); - $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 2)); - $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); - break; - - - case 'smhd': // Sound Media information HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - break; - - - case 'vmhd': // Video Media information HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); - $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); - - $atom_structure['flags']['no_lean_ahead'] = (bool) ($atom_structure['flags_raw'] & 0x001); - break; - - - case 'hdlr': // HanDLeR reference atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['component_type'] = substr($atom_data, 4, 4); - $atom_structure['component_subtype'] = substr($atom_data, 8, 4); - $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); - $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['component_name'] = $this->Pascal2String(substr($atom_data, 24)); - - if (($atom_structure['component_subtype'] == 'STpn') && ($atom_structure['component_manufacturer'] == 'zzzz')) { - $info['video']['dataformat'] = 'quicktimevr'; - } - break; - - - case 'mdhd': // MeDia HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 2)); - $atom_structure['quality'] = getid3_lib::BigEndian2Int(substr($atom_data, 22, 2)); - - if ($atom_structure['time_scale'] == 0) { - $this->error('Corrupt Quicktime file: mdhd.time_scale == zero'); - return false; - } - $info['quicktime']['time_scale'] = ((isset($info['quicktime']['time_scale']) && ($info['quicktime']['time_scale'] < 1000)) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); - - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - $atom_structure['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; - $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); - if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { - $info['comments']['language'][] = $atom_structure['language']; - } - break; - - - case 'pnot': // Preview atom - $atom_structure['modification_date'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // "standard Macintosh format" - $atom_structure['version_number'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x00 - $atom_structure['atom_type'] = substr($atom_data, 6, 4); // usually: 'PICT' - $atom_structure['atom_index'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); // usually: 0x01 - - $atom_structure['modification_date_unix'] = getid3_lib::DateMac2Unix($atom_structure['modification_date']); - break; - - - case 'crgn': // Clipping ReGioN atom - $atom_structure['region_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); // The Region size, Region boundary box, - $atom_structure['boundary_box'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 8)); // and Clipping region data fields - $atom_structure['clipping_data'] = substr($atom_data, 10); // constitute a QuickDraw region. - break; - - - case 'load': // track LOAD settings atom - $atom_structure['preload_start_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - $atom_structure['preload_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['preload_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['default_hints_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - - $atom_structure['default_hints']['double_buffer'] = (bool) ($atom_structure['default_hints_raw'] & 0x0020); - $atom_structure['default_hints']['high_quality'] = (bool) ($atom_structure['default_hints_raw'] & 0x0100); - break; - - - case 'tmcd': // TiMe CoDe atom - case 'chap': // CHAPter list atom - case 'sync': // SYNChronization atom - case 'scpt': // tranSCriPT atom - case 'ssrc': // non-primary SouRCe atom - for ($i = 0; $i < strlen($atom_data); $i += 4) { - @$atom_structure['track_id'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4)); - } - break; - - - case 'elst': // Edit LiST atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - for ($i = 0; $i < $atom_structure['number_entries']; $i++ ) { - $atom_structure['edit_list'][$i]['track_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 0, 4)); - $atom_structure['edit_list'][$i]['media_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 4, 4)); - $atom_structure['edit_list'][$i]['media_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 8 + ($i * 12) + 8, 4)); - } - break; - - - case 'kmat': // compressed MATte atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['matte_data_raw'] = substr($atom_data, 4); - break; - - - case 'ctab': // Color TABle atom - $atom_structure['color_table_seed'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // hardcoded: 0x00000000 - $atom_structure['color_table_flags'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x8000 - $atom_structure['color_table_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)) + 1; - for ($colortableentry = 0; $colortableentry < $atom_structure['color_table_size']; $colortableentry++) { - $atom_structure['color_table'][$colortableentry]['alpha'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 0, 2)); - $atom_structure['color_table'][$colortableentry]['red'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 2, 2)); - $atom_structure['color_table'][$colortableentry]['green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 4, 2)); - $atom_structure['color_table'][$colortableentry]['blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 6, 2)); - } - break; - - - case 'mvhd': // MoVie HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['preferred_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 20, 4)); - $atom_structure['preferred_volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 24, 2)); - $atom_structure['reserved'] = substr($atom_data, 26, 10); - $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 36, 4)); - $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); - $atom_structure['matrix_u'] = getid3_lib::FixedPoint2_30(substr($atom_data, 44, 4)); - $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 48, 4)); - $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); - $atom_structure['matrix_v'] = getid3_lib::FixedPoint2_30(substr($atom_data, 56, 4)); - $atom_structure['matrix_x'] = getid3_lib::FixedPoint16_16(substr($atom_data, 60, 4)); - $atom_structure['matrix_y'] = getid3_lib::FixedPoint16_16(substr($atom_data, 64, 4)); - $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 68, 4)); - $atom_structure['preview_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 72, 4)); - $atom_structure['preview_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 76, 4)); - $atom_structure['poster_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 80, 4)); - $atom_structure['selection_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 84, 4)); - $atom_structure['selection_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 88, 4)); - $atom_structure['current_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 92, 4)); - $atom_structure['next_track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 96, 4)); - - if ($atom_structure['time_scale'] == 0) { - $this->error('Corrupt Quicktime file: mvhd.time_scale == zero'); - return false; - } - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - $info['quicktime']['time_scale'] = ((isset($info['quicktime']['time_scale']) && ($info['quicktime']['time_scale'] < 1000)) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); - $info['quicktime']['display_scale'] = $atom_structure['matrix_a']; - $info['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; - break; - - - case 'tkhd': // TracK HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['trackid'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['reserved1'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['reserved2'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 8)); - $atom_structure['layer'] = getid3_lib::BigEndian2Int(substr($atom_data, 32, 2)); - $atom_structure['alternate_group'] = getid3_lib::BigEndian2Int(substr($atom_data, 34, 2)); - $atom_structure['volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 36, 2)); - $atom_structure['reserved3'] = getid3_lib::BigEndian2Int(substr($atom_data, 38, 2)); -// http://developer.apple.com/library/mac/#documentation/QuickTime/RM/MovieBasics/MTEditing/K-Chapter/11MatrixFunctions.html -// http://developer.apple.com/library/mac/#documentation/QuickTime/qtff/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737 - $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); - $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 44, 4)); - $atom_structure['matrix_u'] = getid3_lib::FixedPoint2_30(substr($atom_data, 48, 4)); - $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); - $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 56, 4)); - $atom_structure['matrix_v'] = getid3_lib::FixedPoint2_30(substr($atom_data, 60, 4)); - $atom_structure['matrix_x'] = getid3_lib::FixedPoint16_16(substr($atom_data, 64, 4)); - $atom_structure['matrix_y'] = getid3_lib::FixedPoint16_16(substr($atom_data, 68, 4)); - $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 72, 4)); - $atom_structure['width'] = getid3_lib::FixedPoint16_16(substr($atom_data, 76, 4)); - $atom_structure['height'] = getid3_lib::FixedPoint16_16(substr($atom_data, 80, 4)); - $atom_structure['flags']['enabled'] = (bool) ($atom_structure['flags_raw'] & 0x0001); - $atom_structure['flags']['in_movie'] = (bool) ($atom_structure['flags_raw'] & 0x0002); - $atom_structure['flags']['in_preview'] = (bool) ($atom_structure['flags_raw'] & 0x0004); - $atom_structure['flags']['in_poster'] = (bool) ($atom_structure['flags_raw'] & 0x0008); - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - - if ($atom_structure['flags']['enabled'] == 1) { - if (!isset($info['video']['resolution_x']) || !isset($info['video']['resolution_y'])) { - $info['video']['resolution_x'] = $atom_structure['width']; - $info['video']['resolution_y'] = $atom_structure['height']; + case 'ctab': // Color TABle atom + $atom_structure['color_table_seed'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // hardcoded: 0x00000000 + $atom_structure['color_table_flags'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x8000 + $atom_structure['color_table_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)) + 1; + for ($colortableentry = 0; $colortableentry < $atom_structure['color_table_size']; $colortableentry++) { + $atom_structure['color_table'][$colortableentry]['alpha'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 0, 2)); + $atom_structure['color_table'][$colortableentry]['red'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 2, 2)); + $atom_structure['color_table'][$colortableentry]['green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 4, 2)); + $atom_structure['color_table'][$colortableentry]['blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 6, 2)); } - $info['video']['resolution_x'] = max($info['video']['resolution_x'], $atom_structure['width']); - $info['video']['resolution_y'] = max($info['video']['resolution_y'], $atom_structure['height']); - $info['quicktime']['video']['resolution_x'] = $info['video']['resolution_x']; - $info['quicktime']['video']['resolution_y'] = $info['video']['resolution_y']; - } else { - // see: http://www.getid3.org/phpBB3/viewtopic.php?t=1295 - //if (isset($info['video']['resolution_x'])) { unset($info['video']['resolution_x']); } - //if (isset($info['video']['resolution_y'])) { unset($info['video']['resolution_y']); } - //if (isset($info['quicktime']['video'])) { unset($info['quicktime']['video']); } - } - break; + break; - case 'iods': // Initial Object DeScriptor atom - // http://www.koders.com/c/fid1FAB3E762903DC482D8A246D4A4BF9F28E049594.aspx?s=windows.h - // http://libquicktime.sourcearchive.com/documentation/1.0.2plus-pdebian/iods_8c-source.html - $offset = 0; - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 3)); - $offset += 3; - $atom_structure['mp4_iod_tag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); - //$offset already adjusted by quicktime_read_mp4_descr_length() - $atom_structure['object_descriptor_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); - $offset += 2; - $atom_structure['od_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['scene_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['audio_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['video_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['graphics_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; + case 'mvhd': // MoVie HeaDer atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); + $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); + $atom_structure['preferred_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 20, 4)); + $atom_structure['preferred_volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 24, 2)); + $atom_structure['reserved'] = substr($atom_data, 26, 10); + $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 36, 4)); + $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); + $atom_structure['matrix_u'] = getid3_lib::FixedPoint2_30(substr($atom_data, 44, 4)); + $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 48, 4)); + $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); + $atom_structure['matrix_v'] = getid3_lib::FixedPoint2_30(substr($atom_data, 56, 4)); + $atom_structure['matrix_x'] = getid3_lib::FixedPoint16_16(substr($atom_data, 60, 4)); + $atom_structure['matrix_y'] = getid3_lib::FixedPoint16_16(substr($atom_data, 64, 4)); + $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 68, 4)); + $atom_structure['preview_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 72, 4)); + $atom_structure['preview_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 76, 4)); + $atom_structure['poster_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 80, 4)); + $atom_structure['selection_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 84, 4)); + $atom_structure['selection_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 88, 4)); + $atom_structure['current_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 92, 4)); + $atom_structure['next_track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 96, 4)); - $atom_structure['num_iods_tracks'] = ($atom_structure['length'] - 7) / 6; // 6 bytes would only be right if all tracks use 1-byte length fields - for ($i = 0; $i < $atom_structure['num_iods_tracks']; $i++) { - $atom_structure['track'][$i]['ES_ID_IncTag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['track'][$i]['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); - //$offset already adjusted by quicktime_read_mp4_descr_length() - $atom_structure['track'][$i]['track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); - $offset += 4; - } + if ($atom_structure['time_scale'] == 0) { + $this->error('Corrupt Quicktime file: mvhd.time_scale == zero'); + return false; + } + $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); + $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); + $info['quicktime']['timestamps_unix']['create'][$atom_structure['hierarchy']] = $atom_structure['creation_time_unix']; + $info['quicktime']['timestamps_unix']['modify'][$atom_structure['hierarchy']] = $atom_structure['modify_time_unix']; + $info['quicktime']['time_scale'] = ((isset($info['quicktime']['time_scale']) && ($info['quicktime']['time_scale'] < 1000)) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); + $info['quicktime']['display_scale'] = $atom_structure['matrix_a']; + $info['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; + break; - $atom_structure['audio_profile_name'] = $this->QuicktimeIODSaudioProfileName($atom_structure['audio_profile_id']); - $atom_structure['video_profile_name'] = $this->QuicktimeIODSvideoProfileName($atom_structure['video_profile_id']); - break; - case 'ftyp': // FileTYPe (?) atom (for MP4 it seems) - $atom_structure['signature'] = substr($atom_data, 0, 4); - $atom_structure['unknown_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['fourcc'] = substr($atom_data, 8, 4); - break; + case 'tkhd': // TracK HeaDer atom + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); + $atom_structure['trackid'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); + $atom_structure['reserved1'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); + $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); + $atom_structure['reserved2'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 8)); + $atom_structure['layer'] = getid3_lib::BigEndian2Int(substr($atom_data, 32, 2)); + $atom_structure['alternate_group'] = getid3_lib::BigEndian2Int(substr($atom_data, 34, 2)); + $atom_structure['volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 36, 2)); + $atom_structure['reserved3'] = getid3_lib::BigEndian2Int(substr($atom_data, 38, 2)); + // http://developer.apple.com/library/mac/#documentation/QuickTime/RM/MovieBasics/MTEditing/K-Chapter/11MatrixFunctions.html + // http://developer.apple.com/library/mac/#documentation/QuickTime/qtff/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737 + $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); + $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 44, 4)); + $atom_structure['matrix_u'] = getid3_lib::FixedPoint2_30(substr($atom_data, 48, 4)); + $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); + $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 56, 4)); + $atom_structure['matrix_v'] = getid3_lib::FixedPoint2_30(substr($atom_data, 60, 4)); + $atom_structure['matrix_x'] = getid3_lib::FixedPoint16_16(substr($atom_data, 64, 4)); + $atom_structure['matrix_y'] = getid3_lib::FixedPoint16_16(substr($atom_data, 68, 4)); + $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 72, 4)); + $atom_structure['width'] = getid3_lib::FixedPoint16_16(substr($atom_data, 76, 4)); + $atom_structure['height'] = getid3_lib::FixedPoint16_16(substr($atom_data, 80, 4)); + $atom_structure['flags']['enabled'] = (bool) ($atom_structure['flags_raw'] & 0x0001); + $atom_structure['flags']['in_movie'] = (bool) ($atom_structure['flags_raw'] & 0x0002); + $atom_structure['flags']['in_preview'] = (bool) ($atom_structure['flags_raw'] & 0x0004); + $atom_structure['flags']['in_poster'] = (bool) ($atom_structure['flags_raw'] & 0x0008); + $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); + $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); + $info['quicktime']['timestamps_unix']['create'][$atom_structure['hierarchy']] = $atom_structure['creation_time_unix']; + $info['quicktime']['timestamps_unix']['modify'][$atom_structure['hierarchy']] = $atom_structure['modify_time_unix']; - case 'mdat': // Media DATa atom - // 'mdat' contains the actual data for the audio/video, possibly also subtitles + // https://www.getid3.org/phpBB3/viewtopic.php?t=1908 + // attempt to compute rotation from matrix values + // 2017-Dec-28: uncertain if 90/270 are correctly oriented; values returned by FixedPoint16_16 should perhaps be -1 instead of 65535(?) + $matrixRotation = 0; + switch ($atom_structure['matrix_a'].':'.$atom_structure['matrix_b'].':'.$atom_structure['matrix_c'].':'.$atom_structure['matrix_d']) { + case '1:0:0:1': $matrixRotation = 0; break; + case '0:1:65535:0': $matrixRotation = 90; break; + case '65535:0:0:65535': $matrixRotation = 180; break; + case '0:65535:1:0': $matrixRotation = 270; break; + default: break; + } -/* due to lack of known documentation, this is a kludge implementation. If you know of documentation on how mdat is properly structed, please send it to info@getid3.org */ + // https://www.getid3.org/phpBB3/viewtopic.php?t=2468 + // The rotation matrix can appear in the Quicktime file multiple times, at least once for each track, + // and it's possible that only the video track (or, in theory, one of the video tracks) is flagged as + // rotated while the other tracks (e.g. audio) is tagged as rotation=0 (behavior noted on iPhone 8 Plus) + // The correct solution would be to check if the TrackID associated with the rotation matrix is indeed + // a video track (or the main video track) and only set the rotation then, but since information about + // what track is what is not trivially there to be examined, the lazy solution is to set the rotation + // if it is found to be nonzero, on the assumption that tracks that don't need it will have rotation set + // to zero (and be effectively ignored) and the video track will have rotation set correctly, which will + // either be zero and automatically correct, or nonzero and be set correctly. + if (!isset($info['video']['rotate']) || (($info['video']['rotate'] == 0) && ($matrixRotation > 0))) { + $info['quicktime']['video']['rotate'] = $info['video']['rotate'] = $matrixRotation; + } - // first, skip any 'wide' padding, and second 'mdat' header (with specified size of zero?) - $mdat_offset = 0; - while (true) { - if (substr($atom_data, $mdat_offset, 8) == "\x00\x00\x00\x08".'wide') { - $mdat_offset += 8; - } elseif (substr($atom_data, $mdat_offset, 8) == "\x00\x00\x00\x00".'mdat') { - $mdat_offset += 8; + if ($atom_structure['flags']['enabled'] == 1) { + if (!isset($info['video']['resolution_x']) || !isset($info['video']['resolution_y'])) { + $info['video']['resolution_x'] = $atom_structure['width']; + $info['video']['resolution_y'] = $atom_structure['height']; + } + $info['video']['resolution_x'] = max($info['video']['resolution_x'], $atom_structure['width']); + $info['video']['resolution_y'] = max($info['video']['resolution_y'], $atom_structure['height']); + $info['quicktime']['video']['resolution_x'] = $info['video']['resolution_x']; + $info['quicktime']['video']['resolution_y'] = $info['video']['resolution_y']; } else { - break; + // see: https://www.getid3.org/phpBB3/viewtopic.php?t=1295 + //if (isset($info['video']['resolution_x'])) { unset($info['video']['resolution_x']); } + //if (isset($info['video']['resolution_y'])) { unset($info['video']['resolution_y']); } + //if (isset($info['quicktime']['video'])) { unset($info['quicktime']['video']); } } - } + break; - // check to see if it looks like chapter titles, in the form of unterminated strings with a leading 16-bit size field - while (($mdat_offset < (strlen($atom_data) - 8)) - && ($chapter_string_length = getid3_lib::BigEndian2Int(substr($atom_data, $mdat_offset, 2))) - && ($chapter_string_length < 1000) - && ($chapter_string_length <= (strlen($atom_data) - $mdat_offset - 2)) - && preg_match('#^([\x00-\xFF]{2})([\x20-\xFF]+)$#', substr($atom_data, $mdat_offset, $chapter_string_length + 2), $chapter_matches)) { - list($dummy, $chapter_string_length_hex, $chapter_string) = $chapter_matches; - $mdat_offset += (2 + $chapter_string_length); - @$info['quicktime']['comments']['chapters'][] = $chapter_string; - // "encd" atom specifies encoding. In theory could be anything, almost always UTF-8, but may be UTF-16 with BOM (not currently handled) - if (substr($atom_data, $mdat_offset, 12) == "\x00\x00\x00\x0C\x65\x6E\x63\x64\x00\x00\x01\x00") { // UTF-8 - $mdat_offset += 12; + case 'iods': // Initial Object DeScriptor atom + // http://www.koders.com/c/fid1FAB3E762903DC482D8A246D4A4BF9F28E049594.aspx?s=windows.h + // http://libquicktime.sourcearchive.com/documentation/1.0.2plus-pdebian/iods_8c-source.html + $offset = 0; + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 3)); + $offset += 3; + $atom_structure['mp4_iod_tag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); + //$offset already adjusted by quicktime_read_mp4_descr_length() + $atom_structure['object_descriptor_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); + $offset += 2; + $atom_structure['od_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['scene_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['audio_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['video_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['graphics_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + + $atom_structure['num_iods_tracks'] = ($atom_structure['length'] - 7) / 6; // 6 bytes would only be right if all tracks use 1-byte length fields + for ($i = 0; $i < $atom_structure['num_iods_tracks']; $i++) { + $atom_structure['track'][$i]['ES_ID_IncTag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); + $offset += 1; + $atom_structure['track'][$i]['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); + //$offset already adjusted by quicktime_read_mp4_descr_length() + $atom_structure['track'][$i]['track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); + $offset += 4; + } + + $atom_structure['audio_profile_name'] = $this->QuicktimeIODSaudioProfileName($atom_structure['audio_profile_id']); + $atom_structure['video_profile_name'] = $this->QuicktimeIODSvideoProfileName($atom_structure['video_profile_id']); + break; + + case 'ftyp': // FileTYPe (?) atom (for MP4 it seems) + $atom_structure['signature'] = substr($atom_data, 0, 4); + $atom_structure['unknown_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $atom_structure['fourcc'] = substr($atom_data, 8, 4); + break; + + case 'mdat': // Media DATa atom + // 'mdat' contains the actual data for the audio/video, possibly also subtitles + + /* due to lack of known documentation, this is a kludge implementation. If you know of documentation on how mdat is properly structed, please send it to info@getid3.org */ + + // first, skip any 'wide' padding, and second 'mdat' header (with specified size of zero?) + $mdat_offset = 0; + while (true) { + if (substr($atom_data, $mdat_offset, 8) == "\x00\x00\x00\x08".'wide') { + $mdat_offset += 8; + } elseif (substr($atom_data, $mdat_offset, 8) == "\x00\x00\x00\x00".'mdat') { + $mdat_offset += 8; + } else { + break; } - } + } + if (substr($atom_data, $mdat_offset, 4) == 'GPRO') { + $GOPRO_chunk_length = getid3_lib::LittleEndian2Int(substr($atom_data, $mdat_offset + 4, 4)); + $GOPRO_offset = 8; + $atom_structure['GPRO']['raw'] = substr($atom_data, $mdat_offset + 8, $GOPRO_chunk_length - 8); + $atom_structure['GPRO']['firmware'] = substr($atom_structure['GPRO']['raw'], 0, 15); + $atom_structure['GPRO']['unknown1'] = substr($atom_structure['GPRO']['raw'], 15, 16); + $atom_structure['GPRO']['unknown2'] = substr($atom_structure['GPRO']['raw'], 31, 32); + $atom_structure['GPRO']['unknown3'] = substr($atom_structure['GPRO']['raw'], 63, 16); + $atom_structure['GPRO']['camera'] = substr($atom_structure['GPRO']['raw'], 79, 32); + $info['quicktime']['camera']['model'] = rtrim($atom_structure['GPRO']['camera'], "\x00"); + } + // check to see if it looks like chapter titles, in the form of unterminated strings with a leading 16-bit size field + while (($mdat_offset < (strlen($atom_data) - 8)) + && ($chapter_string_length = getid3_lib::BigEndian2Int(substr($atom_data, $mdat_offset, 2))) + && ($chapter_string_length < 1000) + && ($chapter_string_length <= (strlen($atom_data) - $mdat_offset - 2)) + && preg_match('#^([\x00-\xFF]{2})([\x20-\xFF]+)$#', substr($atom_data, $mdat_offset, $chapter_string_length + 2), $chapter_matches)) { + list($dummy, $chapter_string_length_hex, $chapter_string) = $chapter_matches; + $mdat_offset += (2 + $chapter_string_length); + @$info['quicktime']['comments']['chapters'][] = $chapter_string; - if (($atomsize > 8) && (!isset($info['avdataend_tmp']) || ($info['quicktime'][$atomname]['size'] > ($info['avdataend_tmp'] - $info['avdataoffset'])))) { + // "encd" atom specifies encoding. In theory could be anything, almost always UTF-8, but may be UTF-16 with BOM (not currently handled) + if (substr($atom_data, $mdat_offset, 12) == "\x00\x00\x00\x0C\x65\x6E\x63\x64\x00\x00\x01\x00") { // UTF-8 + $mdat_offset += 12; + } + } - $info['avdataoffset'] = $atom_structure['offset'] + 8; // $info['quicktime'][$atomname]['offset'] + 8; - $OldAVDataEnd = $info['avdataend']; - $info['avdataend'] = $atom_structure['offset'] + $atom_structure['size']; // $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size']; + if (($atomsize > 8) && (!isset($info['avdataend_tmp']) || ($info['quicktime'][$atomname]['size'] > ($info['avdataend_tmp'] - $info['avdataoffset'])))) { + + $info['avdataoffset'] = $atom_structure['offset'] + 8; // $info['quicktime'][$atomname]['offset'] + 8; + $OldAVDataEnd = $info['avdataend']; + $info['avdataend'] = $atom_structure['offset'] + $atom_structure['size']; // $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size']; + + $getid3_temp = new getID3(); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); + $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; + $getid3_temp->info['avdataend'] = $info['avdataend']; + $getid3_mp3 = new getid3_mp3($getid3_temp); + if ($getid3_mp3->MPEGaudioHeaderValid($getid3_mp3->MPEGaudioHeaderDecode($this->fread(4)))) { + $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); + if (!empty($getid3_temp->info['warning'])) { + foreach ($getid3_temp->info['warning'] as $value) { + $this->warning($value); + } + } + if (!empty($getid3_temp->info['mpeg'])) { + $info['mpeg'] = $getid3_temp->info['mpeg']; + if (isset($info['mpeg']['audio'])) { + $info['audio']['dataformat'] = 'mp3'; + $info['audio']['codec'] = (!empty($info['mpeg']['audio']['encoder']) ? $info['mpeg']['audio']['encoder'] : (!empty($info['mpeg']['audio']['codec']) ? $info['mpeg']['audio']['codec'] : (!empty($info['mpeg']['audio']['LAME']) ? 'LAME' :'mp3'))); + $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; + $info['audio']['channels'] = $info['mpeg']['audio']['channels']; + $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; + $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); + $info['bitrate'] = $info['audio']['bitrate']; + } + } + } + unset($getid3_mp3, $getid3_temp); + $info['avdataend'] = $OldAVDataEnd; + unset($OldAVDataEnd); + + } + + unset($mdat_offset, $chapter_string_length, $chapter_matches); + break; + + case 'ID32': // ID3v2 + getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_mp3 = new getid3_mp3($getid3_temp); - if ($getid3_mp3->MPEGaudioHeaderValid($getid3_mp3->MPEGaudioHeaderDecode($this->fread(4)))) { - $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $value) { - $this->warning($value); - } - } - if (!empty($getid3_temp->info['mpeg'])) { - $info['mpeg'] = $getid3_temp->info['mpeg']; - if (isset($info['mpeg']['audio'])) { - $info['audio']['dataformat'] = 'mp3'; - $info['audio']['codec'] = (!empty($info['mpeg']['audio']['encoder']) ? $info['mpeg']['audio']['encoder'] : (!empty($info['mpeg']['audio']['codec']) ? $info['mpeg']['audio']['codec'] : (!empty($info['mpeg']['audio']['LAME']) ? 'LAME' :'mp3'))); - $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - $info['audio']['channels'] = $info['mpeg']['audio']['channels']; - $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; - $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); - $info['bitrate'] = $info['audio']['bitrate']; - } - } - } - unset($getid3_mp3, $getid3_temp); - $info['avdataend'] = $OldAVDataEnd; - unset($OldAVDataEnd); - - } - - unset($mdat_offset, $chapter_string_length, $chapter_matches); - break; - - case 'free': // FREE space atom - case 'skip': // SKIP atom - case 'wide': // 64-bit expansion placeholder atom - // 'free', 'skip' and 'wide' are just padding, contains no useful data at all - - // When writing QuickTime files, it is sometimes necessary to update an atom's size. - // It is impossible to update a 32-bit atom to a 64-bit atom since the 32-bit atom - // is only 8 bytes in size, and the 64-bit atom requires 16 bytes. Therefore, QuickTime - // puts an 8-byte placeholder atom before any atoms it may have to update the size of. - // In this way, if the atom needs to be converted from a 32-bit to a 64-bit atom, the - // placeholder atom can be overwritten to obtain the necessary 8 extra bytes. - // The placeholder atom has a type of kWideAtomPlaceholderType ( 'wide' ). - break; - - - case 'nsav': // NoSAVe atom - // http://developer.apple.com/technotes/tn/tn2038.html - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - break; - - case 'ctyp': // Controller TYPe atom (seen on QTVR) - // http://homepages.slingshot.co.nz/~helmboy/quicktime/formats/qtm-layout.txt - // some controller names are: - // 0x00 + 'std' for linear movie - // 'none' for no controls - $atom_structure['ctyp'] = substr($atom_data, 0, 4); - $info['quicktime']['controller'] = $atom_structure['ctyp']; - switch ($atom_structure['ctyp']) { - case 'qtvr': - $info['video']['dataformat'] = 'quicktimevr'; - break; - } - break; - - case 'pano': // PANOrama track (seen on QTVR) - $atom_structure['pano'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - break; - - case 'hint': // HINT track - case 'hinf': // - case 'hinv': // - case 'hnti': // - $info['quicktime']['hinting'] = true; - break; - - case 'imgt': // IMaGe Track reference (kQTVRImageTrackRefType) (seen on QTVR) - for ($i = 0; $i < ($atom_structure['size'] - 8); $i += 4) { - $atom_structure['imgt'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4)); - } - break; - - - // Observed-but-not-handled atom types are just listed here to prevent warnings being generated - case 'FXTC': // Something to do with Adobe After Effects (?) - case 'PrmA': - case 'code': - case 'FIEL': // this is NOT "fiel" (Field Ordering) as describe here: http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html - case 'tapt': // TrackApertureModeDimensionsAID - http://developer.apple.com/documentation/QuickTime/Reference/QT7-1_Update_Reference/Constants/Constants.html - // tapt seems to be used to compute the video size [http://www.getid3.org/phpBB3/viewtopic.php?t=838] - // * http://lists.apple.com/archives/quicktime-api/2006/Aug/msg00014.html - // * http://handbrake.fr/irclogs/handbrake-dev/handbrake-dev20080128_pg2.html - case 'ctts':// STCompositionOffsetAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'cslg':// STCompositionShiftLeastGreatestAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'sdtp':// STSampleDependencyAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'stps':// STPartialSyncSampleAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - //$atom_structure['data'] = $atom_data; - break; - - case "\xA9".'xyz': // GPS latitude+longitude+altitude - $atom_structure['data'] = $atom_data; - if (preg_match('#([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)?/$#i', $atom_data, $matches)) { - @list($all, $latitude, $longitude, $altitude) = $matches; - $info['quicktime']['comments']['gps_latitude'][] = floatval($latitude); - $info['quicktime']['comments']['gps_longitude'][] = floatval($longitude); - if (!empty($altitude)) { - $info['quicktime']['comments']['gps_altitude'][] = floatval($altitude); - } - } else { - $this->warning('QuickTime atom "©xyz" data does not match expected data pattern at offset '.$baseoffset.'. Please report as getID3() bug.'); - } - break; - - case 'NCDT': - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms); - break; - case 'NCTH': // Nikon Camera THumbnail image - case 'NCVW': // Nikon Camera preVieW image - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - if (preg_match('/^\xFF\xD8\xFF/', $atom_data)) { - $atom_structure['data'] = $atom_data; - $atom_structure['image_mime'] = 'image/jpeg'; - $atom_structure['description'] = (($atomname == 'NCTH') ? 'Nikon Camera Thumbnail Image' : (($atomname == 'NCVW') ? 'Nikon Camera Preview Image' : 'Nikon preview image')); - $info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_data, 'description'=>$atom_structure['description']); - } - break; - case 'NCTG': // Nikon - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG - $atom_structure['data'] = $this->QuicktimeParseNikonNCTG($atom_data); - break; - case 'NCHD': // Nikon:MakerNoteVersion - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - case 'NCDB': // Nikon - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - case 'CNCV': // Canon:CompressorVersion - http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html - $atom_structure['data'] = $atom_data; - break; - - case "\x00\x00\x00\x00": - // some kind of metacontainer, may contain a big data dump such as: - // mdta keys \005 mdtacom.apple.quicktime.make (mdtacom.apple.quicktime.creationdate ,mdtacom.apple.quicktime.location.ISO6709 $mdtacom.apple.quicktime.software !mdtacom.apple.quicktime.model ilst \01D \001 \015data \001DE\010Apple 0 \002 (data \001DE\0102011-05-11T17:54:04+0200 2 \003 *data \001DE\010+52.4936+013.3897+040.247/ \01D \004 \015data \001DE\0104.3.1 \005 \018data \001DE\010iPhone 4 - // http://www.geocities.com/xhelmboyx/quicktime/formats/qti-layout.txt - - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom(substr($atom_data, 4), $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - //$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'meta': // METAdata atom - // https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html - - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'data': // metaDATA atom - static $metaDATAkey = 1; // real ugly, but so is the QuickTime structure that stores keys and values in different multinested locations that are hard to relate to each other - // seems to be 2 bytes language code (ASCII), 2 bytes unknown (set to 0x10B5 in sample I have), remainder is useful data - $atom_structure['language'] = substr($atom_data, 4 + 0, 2); - $atom_structure['unknown'] = getid3_lib::BigEndian2Int(substr($atom_data, 4 + 2, 2)); - $atom_structure['data'] = substr($atom_data, 4 + 4); - $atom_structure['key_name'] = @$info['quicktime']['temp_meta_key_names'][$metaDATAkey++]; - - if ($atom_structure['key_name'] && $atom_structure['data']) { - @$info['quicktime']['comments'][str_replace('com.apple.quicktime.', '', $atom_structure['key_name'])][] = $atom_structure['data']; - } - break; - - case 'keys': // KEYS that may be present in the metadata atom. - // https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW21 - // The metadata item keys atom holds a list of the metadata keys that may be present in the metadata atom. - // This list is indexed starting with 1; 0 is a reserved index value. The metadata item keys atom is a full atom with an atom type of "keys". - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['entry_count'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $keys_atom_offset = 8; - for ($i = 1; $i <= $atom_structure['entry_count']; $i++) { - $atom_structure['keys'][$i]['key_size'] = getid3_lib::BigEndian2Int(substr($atom_data, $keys_atom_offset + 0, 4)); - $atom_structure['keys'][$i]['key_namespace'] = substr($atom_data, $keys_atom_offset + 4, 4); - $atom_structure['keys'][$i]['key_value'] = substr($atom_data, $keys_atom_offset + 8, $atom_structure['keys'][$i]['key_size'] - 8); - $keys_atom_offset += $atom_structure['keys'][$i]['key_size']; // key_size includes the 4+4 bytes for key_size and key_namespace - - $info['quicktime']['temp_meta_key_names'][$i] = $atom_structure['keys'][$i]['key_value']; - } - break; - - case 'gps ': - // https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730 - // The 'gps ' contains simple look up table made up of 8byte rows, that point to the 'free' atoms that contains the actual GPS data. - // The first row is version/metadata/notsure, I skip that. - // The following rows consist of 4byte address (absolute) and 4byte size (0x1000), these point to the GPS data in the file. - - $GPS_rowsize = 8; // 4 bytes for offset, 4 bytes for size - if (strlen($atom_data) > 0) { - if ((strlen($atom_data) % $GPS_rowsize) == 0) { - $atom_structure['gps_toc'] = array(); - foreach (str_split($atom_data, $GPS_rowsize) as $counter => $datapair) { - $atom_structure['gps_toc'][] = unpack('Noffset/Nsize', substr($atom_data, $counter * $GPS_rowsize, $GPS_rowsize)); - } - - $atom_structure['gps_entries'] = array(); - $previous_offset = $this->ftell(); - foreach ($atom_structure['gps_toc'] as $key => $gps_pointer) { - if ($key == 0) { - // "The first row is version/metadata/notsure, I skip that." - continue; - } - $this->fseek($gps_pointer['offset']); - $GPS_free_data = $this->fread($gps_pointer['size']); - - /* - // 2017-05-10: I see some of the data, notably the Hour-Minute-Second, but cannot reconcile the rest of the data. However, the NMEA "GPRMC" line is there and relatively easy to parse, so I'm using that instead - - // https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730 - // The structure of the GPS data atom (the 'free' atoms mentioned above) is following: - // hour,minute,second,year,month,day,active,latitude_b,longitude_b,unknown2,latitude,longitude,speed = struct.unpack_from(' 90) ? 1900 : 2000); // complete lack of foresight: datestamps are stored with 2-digit years, take best guess - $GPS_this_GPRMC['timestamp'] = $year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second.$ms; - - $GPS_this_GPRMC['active'] = ($GPS_this_GPRMC['raw']['status'] == 'A'); // A=Active,V=Void - - foreach (array('latitude','longitude') as $latlon) { - preg_match('#^([0-9]{1,3})([0-9]{2}\\.[0-9]+)$#', $GPS_this_GPRMC['raw'][$latlon], $matches); - list($dummy, $deg, $min) = $matches; - $GPS_this_GPRMC[$latlon] = $deg + ($min / 60); - } - $GPS_this_GPRMC['latitude'] *= (($GPS_this_GPRMC['raw']['latitude_direction'] == 'S') ? -1 : 1); - $GPS_this_GPRMC['longitude'] *= (($GPS_this_GPRMC['raw']['longitude_direction'] == 'W') ? -1 : 1); - - $GPS_this_GPRMC['heading'] = $GPS_this_GPRMC['raw']['angle']; - $GPS_this_GPRMC['speed_knot'] = $GPS_this_GPRMC['raw']['knots']; - $GPS_this_GPRMC['speed_kmh'] = $GPS_this_GPRMC['raw']['knots'] * 1.852; - if ($GPS_this_GPRMC['raw']['variation']) { - $GPS_this_GPRMC['variation'] = $GPS_this_GPRMC['raw']['variation']; - $GPS_this_GPRMC['variation'] *= (($GPS_this_GPRMC['raw']['variation_direction'] == 'W') ? -1 : 1); - } - - $atom_structure['gps_entries'][$key] = $GPS_this_GPRMC; - - @$info['quicktime']['gps_track'][$GPS_this_GPRMC['timestamp']] = array( - 'latitude' => $GPS_this_GPRMC['latitude'], - 'longitude' => $GPS_this_GPRMC['longitude'], - 'speed_kmh' => $GPS_this_GPRMC['speed_kmh'], - 'heading' => $GPS_this_GPRMC['heading'], - ); - - } else { - $this->warning('Unhandled GPS format in "free" atom at offset '.$gps_pointer['offset']); - } - } - $this->fseek($previous_offset); - + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); + $getid3_id3v2 = new getid3_id3v2($getid3_temp); + $getid3_id3v2->StartingOffset = $atom_structure['offset'] + 14; // framelength(4)+framename(4)+flags(4)+??(2) + if ($atom_structure['valid'] = $getid3_id3v2->Analyze()) { + $atom_structure['id3v2'] = $getid3_temp->info['id3v2']; } else { - $this->warning('QuickTime atom "'.$atomname.'" is not mod-8 bytes long ('.$atomsize.' bytes) at offset '.$baseoffset); + $this->warning('ID32 frame at offset '.$atom_structure['offset'].' did not parse'); } - } else { - $this->warning('QuickTime atom "'.$atomname.'" is zero bytes long at offset '.$baseoffset); - } - break; + unset($getid3_temp, $getid3_id3v2); + break; - case 'loci':// 3GP location (El Loco) - $loffset = 0; - $info['quicktime']['comments']['gps_flags'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - $info['quicktime']['comments']['gps_lang'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $info['quicktime']['comments']['gps_location'] = $this->LociString(substr($atom_data, 6), $loffset); - $loci_data = substr($atom_data, 6 + $loffset); - $info['quicktime']['comments']['gps_role'] = getid3_lib::BigEndian2Int(substr($loci_data, 0, 1)); - $info['quicktime']['comments']['gps_longitude'] = getid3_lib::FixedPoint16_16(substr($loci_data, 1, 4)); - $info['quicktime']['comments']['gps_latitude'] = getid3_lib::FixedPoint16_16(substr($loci_data, 5, 4)); - $info['quicktime']['comments']['gps_altitude'] = getid3_lib::FixedPoint16_16(substr($loci_data, 9, 4)); - $info['quicktime']['comments']['gps_body'] = $this->LociString(substr($loci_data, 13 ), $loffset); - $info['quicktime']['comments']['gps_notes'] = $this->LociString(substr($loci_data, 13 + $loffset), $loffset); - break; + case 'free': // FREE space atom + case 'skip': // SKIP atom + case 'wide': // 64-bit expansion placeholder atom + // 'free', 'skip' and 'wide' are just padding, contains no useful data at all - case 'chpl': // CHaPter List - // https://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf - $chpl_version = getid3_lib::BigEndian2Int(substr($atom_data, 4, 1)); // Expected to be 0 - $chpl_flags = getid3_lib::BigEndian2Int(substr($atom_data, 5, 3)); // Reserved, set to 0 - $chpl_count = getid3_lib::BigEndian2Int(substr($atom_data, 8, 1)); - $chpl_offset = 9; - for ($i = 0; $i < $chpl_count; $i++) { - if (($chpl_offset + 9) >= strlen($atom_data)) { - $this->warning('QuickTime chapter '.$i.' extends beyond end of "chpl" atom'); - break; + // When writing QuickTime files, it is sometimes necessary to update an atom's size. + // It is impossible to update a 32-bit atom to a 64-bit atom since the 32-bit atom + // is only 8 bytes in size, and the 64-bit atom requires 16 bytes. Therefore, QuickTime + // puts an 8-byte placeholder atom before any atoms it may have to update the size of. + // In this way, if the atom needs to be converted from a 32-bit to a 64-bit atom, the + // placeholder atom can be overwritten to obtain the necessary 8 extra bytes. + // The placeholder atom has a type of kWideAtomPlaceholderType ( 'wide' ). + break; + + + case 'nsav': // NoSAVe atom + // http://developer.apple.com/technotes/tn/tn2038.html + $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); + break; + + case 'ctyp': // Controller TYPe atom (seen on QTVR) + // http://homepages.slingshot.co.nz/~helmboy/quicktime/formats/qtm-layout.txt + // some controller names are: + // 0x00 + 'std' for linear movie + // 'none' for no controls + $atom_structure['ctyp'] = substr($atom_data, 0, 4); + $info['quicktime']['controller'] = $atom_structure['ctyp']; + switch ($atom_structure['ctyp']) { + case 'qtvr': + $info['video']['dataformat'] = 'quicktimevr'; + break; } - $info['quicktime']['chapters'][$i]['timestamp'] = getid3_lib::BigEndian2Int(substr($atom_data, $chpl_offset, 8)) / 10000000; // timestamps are stored as 100-nanosecond units - $chpl_offset += 8; - $chpl_title_size = getid3_lib::BigEndian2Int(substr($atom_data, $chpl_offset, 1)); - $chpl_offset += 1; - $info['quicktime']['chapters'][$i]['title'] = substr($atom_data, $chpl_offset, $chpl_title_size); - $chpl_offset += $chpl_title_size; - } - break; + break; - default: - $this->warning('Unknown QuickTime atom type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" ('.trim(getid3_lib::PrintHexBytes($atomname)).') at offset '.$baseoffset); - $atom_structure['data'] = $atom_data; - break; + case 'pano': // PANOrama track (seen on QTVR) + $atom_structure['pano'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); + break; + + case 'hint': // HINT track + case 'hinf': // + case 'hinv': // + case 'hnti': // + $info['quicktime']['hinting'] = true; + break; + + case 'imgt': // IMaGe Track reference (kQTVRImageTrackRefType) (seen on QTVR) + for ($i = 0; $i < ($atom_structure['size'] - 8); $i += 4) { + $atom_structure['imgt'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4)); + } + break; + + + // Observed-but-not-handled atom types are just listed here to prevent warnings being generated + case 'FXTC': // Something to do with Adobe After Effects (?) + case 'PrmA': + case 'code': + case 'FIEL': // this is NOT "fiel" (Field Ordering) as describe here: http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html + case 'tapt': // TrackApertureModeDimensionsAID - http://developer.apple.com/documentation/QuickTime/Reference/QT7-1_Update_Reference/Constants/Constants.html + // tapt seems to be used to compute the video size [https://www.getid3.org/phpBB3/viewtopic.php?t=838] + // * http://lists.apple.com/archives/quicktime-api/2006/Aug/msg00014.html + // * http://handbrake.fr/irclogs/handbrake-dev/handbrake-dev20080128_pg2.html + case 'ctts':// STCompositionOffsetAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html + case 'cslg':// STCompositionShiftLeastGreatestAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html + case 'sdtp':// STSampleDependencyAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html + case 'stps':// STPartialSyncSampleAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html + //$atom_structure['data'] = $atom_data; + break; + + case "\xA9".'xyz': // GPS latitude+longitude+altitude + $atom_structure['data'] = $atom_data; + if (preg_match('#([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)?/$#i', $atom_data, $matches)) { + @list($all, $latitude, $longitude, $altitude) = $matches; + $info['quicktime']['comments']['gps_latitude'][] = floatval($latitude); + $info['quicktime']['comments']['gps_longitude'][] = floatval($longitude); + if (!empty($altitude)) { + $info['quicktime']['comments']['gps_altitude'][] = floatval($altitude); + } + } else { + $this->warning('QuickTime atom "©xyz" data does not match expected data pattern at offset '.$baseoffset.'. Please report as getID3() bug.'); + } + break; + + case 'NCDT': + // https://exiftool.org/TagNames/Nikon.html + // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms); + break; + case 'NCTH': // Nikon Camera THumbnail image + case 'NCVW': // Nikon Camera preVieW image + case 'NCM1': // Nikon Camera preview iMage 1 + case 'NCM2': // Nikon Camera preview iMage 2 + // https://exiftool.org/TagNames/Nikon.html + if (preg_match('/^\xFF\xD8\xFF/', $atom_data)) { + $descriptions = array( + 'NCTH' => 'Nikon Camera Thumbnail Image', + 'NCVW' => 'Nikon Camera Preview Image', + 'NCM1' => 'Nikon Camera Preview Image 1', + 'NCM2' => 'Nikon Camera Preview Image 2', + ); + $atom_structure['data'] = $atom_data; + $atom_structure['image_mime'] = 'image/jpeg'; + $atom_structure['description'] = $descriptions[$atomname]; + $info['quicktime']['comments']['picture'][] = array( + 'image_mime' => $atom_structure['image_mime'], + 'data' => $atom_data, + 'description' => $atom_structure['description'] + ); + } + break; + case 'NCTG': // Nikon - https://exiftool.org/TagNames/Nikon.html#NCTG + getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.nikon-nctg.php', __FILE__, true); + $nikonNCTG = new getid3_tag_nikon_nctg($this->getid3); + + $atom_structure['data'] = $nikonNCTG->parse($atom_data); + break; + case 'NCHD': // Nikon:MakerNoteVersion - https://exiftool.org/TagNames/Nikon.html + $makerNoteVersion = ''; + for ($i = 0, $iMax = strlen($atom_data); $i < $iMax; ++$i) { + if (ord($atom_data[$i]) <= 0x1F) { + $makerNoteVersion .= ' '.ord($atom_data[$i]); + } else { + $makerNoteVersion .= $atom_data[$i]; + } + } + $makerNoteVersion = rtrim($makerNoteVersion, "\x00"); + $atom_structure['data'] = array( + 'MakerNoteVersion' => $makerNoteVersion + ); + break; + case 'NCDB': // Nikon - https://exiftool.org/TagNames/Nikon.html + case 'CNCV': // Canon:CompressorVersion - https://exiftool.org/TagNames/Canon.html + $atom_structure['data'] = $atom_data; + break; + + case "\x00\x00\x00\x00": + // some kind of metacontainer, may contain a big data dump such as: + // mdta keys \005 mdtacom.apple.quicktime.make (mdtacom.apple.quicktime.creationdate ,mdtacom.apple.quicktime.location.ISO6709 $mdtacom.apple.quicktime.software !mdtacom.apple.quicktime.model ilst \01D \001 \015data \001DE\010Apple 0 \002 (data \001DE\0102011-05-11T17:54:04+0200 2 \003 *data \001DE\010+52.4936+013.3897+040.247/ \01D \004 \015data \001DE\0104.3.1 \005 \018data \001DE\010iPhone 4 + // https://xhelmboyx.tripod.com/formats/qti-layout.txt + + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom(substr($atom_data, 4), $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + //$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + break; + + case 'meta': // METAdata atom + // https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html + + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); + break; + + case 'data': // metaDATA atom + static $metaDATAkey = 1; // real ugly, but so is the QuickTime structure that stores keys and values in different multinested locations that are hard to relate to each other + // seems to be 2 bytes language code (ASCII), 2 bytes unknown (set to 0x10B5 in sample I have), remainder is useful data + $atom_structure['language'] = substr($atom_data, 4 + 0, 2); + $atom_structure['unknown'] = getid3_lib::BigEndian2Int(substr($atom_data, 4 + 2, 2)); + $atom_structure['data'] = substr($atom_data, 4 + 4); + $atom_structure['key_name'] = (isset($info['quicktime']['temp_meta_key_names'][$metaDATAkey]) ? $info['quicktime']['temp_meta_key_names'][$metaDATAkey] : ''); + $metaDATAkey++; + + if ($atom_structure['key_name'] && $atom_structure['data']) { + @$info['quicktime']['comments'][str_replace('com.apple.quicktime.', '', $atom_structure['key_name'])][] = $atom_structure['data']; + } + break; + + case 'keys': // KEYS that may be present in the metadata atom. + // https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW21 + // The metadata item keys atom holds a list of the metadata keys that may be present in the metadata atom. + // This list is indexed starting with 1; 0 is a reserved index value. The metadata item keys atom is a full atom with an atom type of "keys". + $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); + $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); + $atom_structure['entry_count'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); + $keys_atom_offset = 8; + for ($i = 1; $i <= $atom_structure['entry_count']; $i++) { + $atom_structure['keys'][$i]['key_size'] = getid3_lib::BigEndian2Int(substr($atom_data, $keys_atom_offset + 0, 4)); + $atom_structure['keys'][$i]['key_namespace'] = substr($atom_data, $keys_atom_offset + 4, 4); + $atom_structure['keys'][$i]['key_value'] = substr($atom_data, $keys_atom_offset + 8, $atom_structure['keys'][$i]['key_size'] - 8); + $keys_atom_offset += $atom_structure['keys'][$i]['key_size']; // key_size includes the 4+4 bytes for key_size and key_namespace + + $info['quicktime']['temp_meta_key_names'][$i] = $atom_structure['keys'][$i]['key_value']; + } + break; + + case 'uuid': // user-defined atom often seen containing XML data, also used for potentially many other purposes, only a few specifically handled by getID3 (e.g. 360fly spatial data) + //Get the UUID ID in first 16 bytes + $uuid_bytes_read = unpack('H8time_low/H4time_mid/H4time_hi/H4clock_seq_hi/H12clock_seq_low', substr($atom_data, 0, 16)); + $atom_structure['uuid_field_id'] = implode('-', $uuid_bytes_read); + + switch ($atom_structure['uuid_field_id']) { // http://fileformats.archiveteam.org/wiki/Boxes/atoms_format#UUID_boxes + + case '0537cdab-9d0c-4431-a72a-fa561f2a113e': // Exif - http://fileformats.archiveteam.org/wiki/Exif + case '2c4c0100-8504-40b9-a03e-562148d6dfeb': // Photoshop Image Resources - http://fileformats.archiveteam.org/wiki/Photoshop_Image_Resources + case '33c7a4d2-b81d-4723-a0ba-f1a3e097ad38': // IPTC-IIM - http://fileformats.archiveteam.org/wiki/IPTC-IIM + case '8974dbce-7be7-4c51-84f9-7148f9882554': // PIFF Track Encryption Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format + case '96a9f1f1-dc98-402d-a7ae-d68e34451809': // GeoJP2 World File Box - http://fileformats.archiveteam.org/wiki/GeoJP2 + case 'a2394f52-5a9b-4f14-a244-6c427c648df4': // PIFF Sample Encryption Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format + case 'b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03': // GeoJP2 GeoTIFF Box - http://fileformats.archiveteam.org/wiki/GeoJP2 + case 'd08a4f18-10f3-4a82-b6c8-32d8aba183d3': // PIFF Protection System Specific Header Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format + $this->warning('Unhandled (but recognized) "uuid" atom identified by "'.$atom_structure['uuid_field_id'].'" at offset '.$atom_structure['offset'].' ('.strlen($atom_data).' bytes)'); + break; + + case 'be7acfcb-97a9-42e8-9c71-999491e3afac': // XMP data (in XML format) + $atom_structure['xml'] = substr($atom_data, 16, strlen($atom_data) - 16 - 8); // 16 bytes for UUID, 8 bytes header(?) + break; + + case 'efe1589a-bb77-49ef-8095-27759eb1dc6f': // 360fly data + /* 360fly code in this block by Paul Lewis 2019-Oct-31 */ + /* Sensor Timestamps need to be calculated using the recordings base time at ['quicktime']['moov']['subatoms'][0]['creation_time_unix']. */ + $atom_structure['title'] = '360Fly Sensor Data'; + + //Get the UUID HEADER data + $uuid_bytes_read = unpack('vheader_size/vheader_version/vtimescale/vhardware_version/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/', substr($atom_data, 16, 32)); + $atom_structure['uuid_header'] = $uuid_bytes_read; + + $start_byte = 48; + $atom_SENSOR_data = substr($atom_data, $start_byte); + $atom_structure['sensor_data']['data_type'] = array( + 'fusion_count' => 0, // ID 250 + 'fusion_data' => array(), + 'accel_count' => 0, // ID 1 + 'accel_data' => array(), + 'gyro_count' => 0, // ID 2 + 'gyro_data' => array(), + 'magno_count' => 0, // ID 3 + 'magno_data' => array(), + 'gps_count' => 0, // ID 5 + 'gps_data' => array(), + 'rotation_count' => 0, // ID 6 + 'rotation_data' => array(), + 'unknown_count' => 0, // ID ?? + 'unknown_data' => array(), + 'debug_list' => '', // Used to debug variables stored as comma delimited strings + ); + $debug_structure = array(); + $debug_structure['debug_items'] = array(); + // Can start loop here to decode all sensor data in 32 Byte chunks: + foreach (str_split($atom_SENSOR_data, 32) as $sensor_key => $sensor_data) { + // This gets me a data_type code to work out what data is in the next 31 bytes. + $sensor_data_type = substr($sensor_data, 0, 1); + $sensor_data_content = substr($sensor_data, 1); + $uuid_bytes_read = unpack('C*', $sensor_data_type); + $sensor_data_array = array(); + switch ($uuid_bytes_read[1]) { + case 250: + $atom_structure['sensor_data']['data_type']['fusion_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['yaw'] = $uuid_bytes_read['yaw']; + $sensor_data_array['pitch'] = $uuid_bytes_read['pitch']; + $sensor_data_array['roll'] = $uuid_bytes_read['roll']; + array_push($atom_structure['sensor_data']['data_type']['fusion_data'], $sensor_data_array); + break; + case 1: + $atom_structure['sensor_data']['data_type']['accel_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['yaw'] = $uuid_bytes_read['yaw']; + $sensor_data_array['pitch'] = $uuid_bytes_read['pitch']; + $sensor_data_array['roll'] = $uuid_bytes_read['roll']; + array_push($atom_structure['sensor_data']['data_type']['accel_data'], $sensor_data_array); + break; + case 2: + $atom_structure['sensor_data']['data_type']['gyro_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['yaw'] = $uuid_bytes_read['yaw']; + $sensor_data_array['pitch'] = $uuid_bytes_read['pitch']; + $sensor_data_array['roll'] = $uuid_bytes_read['roll']; + array_push($atom_structure['sensor_data']['data_type']['gyro_data'], $sensor_data_array); + break; + case 3: + $atom_structure['sensor_data']['data_type']['magno_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Gmagx/Gmagy/Gmagz/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['magx'] = $uuid_bytes_read['magx']; + $sensor_data_array['magy'] = $uuid_bytes_read['magy']; + $sensor_data_array['magz'] = $uuid_bytes_read['magz']; + array_push($atom_structure['sensor_data']['data_type']['magno_data'], $sensor_data_array); + break; + case 5: + $atom_structure['sensor_data']['data_type']['gps_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Glat/Glon/Galt/Gspeed/nbearing/nacc/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['lat'] = $uuid_bytes_read['lat']; + $sensor_data_array['lon'] = $uuid_bytes_read['lon']; + $sensor_data_array['alt'] = $uuid_bytes_read['alt']; + $sensor_data_array['speed'] = $uuid_bytes_read['speed']; + $sensor_data_array['bearing'] = $uuid_bytes_read['bearing']; + $sensor_data_array['acc'] = $uuid_bytes_read['acc']; + array_push($atom_structure['sensor_data']['data_type']['gps_data'], $sensor_data_array); + //array_push($debug_structure['debug_items'], $uuid_bytes_read['timestamp']); + break; + case 6: + $atom_structure['sensor_data']['data_type']['rotation_count']++; + $uuid_bytes_read = unpack('cmode/Jtimestamp/Grotx/Groty/Grotz/x*', $sensor_data_content); + $sensor_data_array['mode'] = $uuid_bytes_read['mode']; + $sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp']; + $sensor_data_array['rotx'] = $uuid_bytes_read['rotx']; + $sensor_data_array['roty'] = $uuid_bytes_read['roty']; + $sensor_data_array['rotz'] = $uuid_bytes_read['rotz']; + array_push($atom_structure['sensor_data']['data_type']['rotation_data'], $sensor_data_array); + break; + default: + $atom_structure['sensor_data']['data_type']['unknown_count']++; + break; + } + } + //if (isset($debug_structure['debug_items']) && count($debug_structure['debug_items']) > 0) { + // $atom_structure['sensor_data']['data_type']['debug_list'] = implode(',', $debug_structure['debug_items']); + //} else { + $atom_structure['sensor_data']['data_type']['debug_list'] = 'No debug items in list!'; + //} + break; + + default: + $this->warning('Unhandled "uuid" atom identified by "'.$atom_structure['uuid_field_id'].'" at offset '.$atom_structure['offset'].' ('.strlen($atom_data).' bytes)'); + } + break; + + case 'gps ': + // https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730 + // The 'gps ' contains simple look up table made up of 8byte rows, that point to the 'free' atoms that contains the actual GPS data. + // The first row is version/metadata/notsure, I skip that. + // The following rows consist of 4byte address (absolute) and 4byte size (0x1000), these point to the GPS data in the file. + + $GPS_rowsize = 8; // 4 bytes for offset, 4 bytes for size + if (strlen($atom_data) > 0) { + if ((strlen($atom_data) % $GPS_rowsize) == 0) { + $atom_structure['gps_toc'] = array(); + foreach (str_split($atom_data, $GPS_rowsize) as $counter => $datapair) { + $atom_structure['gps_toc'][] = unpack('Noffset/Nsize', substr($atom_data, $counter * $GPS_rowsize, $GPS_rowsize)); + } + + $atom_structure['gps_entries'] = array(); + $previous_offset = $this->ftell(); + foreach ($atom_structure['gps_toc'] as $key => $gps_pointer) { + if ($key == 0) { + // "The first row is version/metadata/notsure, I skip that." + continue; + } + $this->fseek($gps_pointer['offset']); + $GPS_free_data = $this->fread($gps_pointer['size']); + + /* + // 2017-05-10: I see some of the data, notably the Hour-Minute-Second, but cannot reconcile the rest of the data. However, the NMEA "GPRMC" line is there and relatively easy to parse, so I'm using that instead + + // https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730 + // The structure of the GPS data atom (the 'free' atoms mentioned above) is following: + // hour,minute,second,year,month,day,active,latitude_b,longitude_b,unknown2,latitude,longitude,speed = struct.unpack_from(' 90) ? 1900 : 2000); // complete lack of foresight: datestamps are stored with 2-digit years, take best guess + $GPS_this_GPRMC['timestamp'] = $year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second.$ms; + + $GPS_this_GPRMC['active'] = ($GPS_this_GPRMC['raw']['status'] == 'A'); // A=Active,V=Void + + foreach (array('latitude','longitude') as $latlon) { + preg_match('#^([0-9]{1,3})([0-9]{2}\\.[0-9]+)$#', $GPS_this_GPRMC['raw'][$latlon], $matches); + list($dummy, $deg, $min) = $matches; + $GPS_this_GPRMC[$latlon] = $deg + ($min / 60); + } + $GPS_this_GPRMC['latitude'] *= (($GPS_this_GPRMC['raw']['latitude_direction'] == 'S') ? -1 : 1); + $GPS_this_GPRMC['longitude'] *= (($GPS_this_GPRMC['raw']['longitude_direction'] == 'W') ? -1 : 1); + + $GPS_this_GPRMC['heading'] = $GPS_this_GPRMC['raw']['angle']; + $GPS_this_GPRMC['speed_knot'] = $GPS_this_GPRMC['raw']['knots']; + $GPS_this_GPRMC['speed_kmh'] = $GPS_this_GPRMC['raw']['knots'] * 1.852; + if ($GPS_this_GPRMC['raw']['variation']) { + $GPS_this_GPRMC['variation'] = $GPS_this_GPRMC['raw']['variation']; + $GPS_this_GPRMC['variation'] *= (($GPS_this_GPRMC['raw']['variation_direction'] == 'W') ? -1 : 1); + } + + $atom_structure['gps_entries'][$key] = $GPS_this_GPRMC; + + @$info['quicktime']['gps_track'][$GPS_this_GPRMC['timestamp']] = array( + 'latitude' => (float) $GPS_this_GPRMC['latitude'], + 'longitude' => (float) $GPS_this_GPRMC['longitude'], + 'speed_kmh' => (float) $GPS_this_GPRMC['speed_kmh'], + 'heading' => (float) $GPS_this_GPRMC['heading'], + ); + + } else { + $this->warning('Unhandled GPS format in "free" atom at offset '.$gps_pointer['offset']); + } + } + $this->fseek($previous_offset); + + } else { + $this->warning('QuickTime atom "'.$atomname.'" is not mod-8 bytes long ('.$atomsize.' bytes) at offset '.$baseoffset); + } + } else { + $this->warning('QuickTime atom "'.$atomname.'" is zero bytes long at offset '.$baseoffset); + } + break; + + case 'loci':// 3GP location (El Loco) + $loffset = 0; + $info['quicktime']['comments']['gps_flags'] = array( getid3_lib::BigEndian2Int(substr($atom_data, 0, 4))); + $info['quicktime']['comments']['gps_lang'] = array( getid3_lib::BigEndian2Int(substr($atom_data, 4, 2))); + $info['quicktime']['comments']['gps_location'] = array( $this->LociString(substr($atom_data, 6), $loffset)); + $loci_data = substr($atom_data, 6 + $loffset); + $info['quicktime']['comments']['gps_role'] = array( getid3_lib::BigEndian2Int(substr($loci_data, 0, 1))); + $info['quicktime']['comments']['gps_longitude'] = array(getid3_lib::FixedPoint16_16(substr($loci_data, 1, 4))); + $info['quicktime']['comments']['gps_latitude'] = array(getid3_lib::FixedPoint16_16(substr($loci_data, 5, 4))); + $info['quicktime']['comments']['gps_altitude'] = array(getid3_lib::FixedPoint16_16(substr($loci_data, 9, 4))); + $info['quicktime']['comments']['gps_body'] = array( $this->LociString(substr($loci_data, 13 ), $loffset)); + $info['quicktime']['comments']['gps_notes'] = array( $this->LociString(substr($loci_data, 13 + $loffset), $loffset)); + break; + + case 'chpl': // CHaPter List + // https://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf + $chpl_version = getid3_lib::BigEndian2Int(substr($atom_data, 4, 1)); // Expected to be 0 + $chpl_flags = getid3_lib::BigEndian2Int(substr($atom_data, 5, 3)); // Reserved, set to 0 + $chpl_count = getid3_lib::BigEndian2Int(substr($atom_data, 8, 1)); + $chpl_offset = 9; + for ($i = 0; $i < $chpl_count; $i++) { + if (($chpl_offset + 9) >= strlen($atom_data)) { + $this->warning('QuickTime chapter '.$i.' extends beyond end of "chpl" atom'); + break; + } + $info['quicktime']['chapters'][$i]['timestamp'] = getid3_lib::BigEndian2Int(substr($atom_data, $chpl_offset, 8)) / 10000000; // timestamps are stored as 100-nanosecond units + $chpl_offset += 8; + $chpl_title_size = getid3_lib::BigEndian2Int(substr($atom_data, $chpl_offset, 1)); + $chpl_offset += 1; + $info['quicktime']['chapters'][$i]['title'] = substr($atom_data, $chpl_offset, $chpl_title_size); + $chpl_offset += $chpl_title_size; + } + break; + + case 'FIRM': // FIRMware version(?), seen on GoPro Hero4 + $info['quicktime']['camera']['firmware'] = $atom_data; + break; + + case 'CAME': // FIRMware version(?), seen on GoPro Hero4 + $info['quicktime']['camera']['serial_hash'] = unpack('H*', $atom_data); + break; + + case 'dscp': + case 'rcif': + // https://www.getid3.org/phpBB3/viewtopic.php?t=1908 + if (substr($atom_data, 0, 7) == "\x00\x00\x00\x00\x55\xC4".'{') { + if ($json_decoded = @json_decode(rtrim(substr($atom_data, 6), "\x00"), true)) { + $info['quicktime']['camera'][$atomname] = $json_decoded; + if (($atomname == 'rcif') && isset($info['quicktime']['camera']['rcif']['wxcamera']['rotate'])) { + $info['video']['rotate'] = $info['quicktime']['video']['rotate'] = $info['quicktime']['camera']['rcif']['wxcamera']['rotate']; + } + } else { + $this->warning('Failed to JSON decode atom "'.$atomname.'"'); + $atom_structure['data'] = $atom_data; + } + unset($json_decoded); + } else { + $this->warning('Expecting 55 C4 7B at start of atom "'.$atomname.'", found '.getid3_lib::PrintHexBytes(substr($atom_data, 4, 3)).' instead'); + $atom_structure['data'] = $atom_data; + } + break; + + case 'frea': + // https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Kodak.html#frea + // may contain "scra" (PreviewImage) and/or "thma" (ThumbnailImage) + $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms); + break; + case 'tima': // subatom to "frea" + // no idea what this does, the one sample file I've seen has a value of 0x00000027 + $atom_structure['data'] = $atom_data; + break; + case 'ver ': // subatom to "frea" + // some kind of version number, the one sample file I've seen has a value of "3.00.073" + $atom_structure['data'] = $atom_data; + break; + case 'thma': // subatom to "frea" -- "ThumbnailImage" + // https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Kodak.html#frea + if (strlen($atom_data) > 0) { + $info['quicktime']['comments']['picture'][] = array('data'=>$atom_data, 'image_mime'=>'image/jpeg', 'description'=>'ThumbnailImage'); + } + break; + case 'scra': // subatom to "frea" -- "PreviewImage" + // https://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Kodak.html#frea + // but the only sample file I've seen has no useful data here + if (strlen($atom_data) > 0) { + $info['quicktime']['comments']['picture'][] = array('data'=>$atom_data, 'image_mime'=>'image/jpeg', 'description'=>'PreviewImage'); + } + break; + + case 'cdsc': // timed metadata reference + // A QuickTime movie can contain none, one, or several timed metadata tracks. Timed metadata tracks can refer to multiple tracks. + // Metadata tracks are linked to the tracks they describe using a track-reference of type 'cdsc'. The metadata track holds the 'cdsc' track reference. + $atom_structure['track_number'] = getid3_lib::BigEndian2Int($atom_data); + break; + + +// AVIF-related - https://docs.rs/avif-parse/0.13.2/src/avif_parse/boxes.rs.html + case 'pitm': // Primary ITeM + case 'iloc': // Item LOCation + case 'iinf': // Item INFo + case 'iref': // Image REFerence + case 'iprp': // Image PRoPerties +$this->error('AVIF files not currently supported'); + $atom_structure['data'] = $atom_data; + break; + + case 'tfdt': // Track Fragment base media Decode Time box + case 'tfhd': // Track Fragment HeaDer box + case 'mfhd': // Movie Fragment HeaDer box + case 'trun': // Track fragment RUN box +$this->error('fragmented mp4 files not currently supported'); + $atom_structure['data'] = $atom_data; + break; + + case 'mvex': // MoVie EXtends box + case 'pssh': // Protection System Specific Header box + case 'sidx': // Segment InDeX box + default: + $this->warning('Unknown QuickTime atom type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" ('.trim(getid3_lib::PrintHexBytes($atomname)).'), '.$atomsize.' bytes at offset '.$baseoffset); + $atom_structure['data'] = $atom_data; + break; + } } array_pop($atomHierarchy); return $atom_structure; } + /** + * @param string $atom_data + * @param int $baseoffset + * @param array $atomHierarchy + * @param bool $ParseAllPossibleAtoms + * + * @return array|false + */ public function QuicktimeParseContainerAtom($atom_data, $baseoffset, &$atomHierarchy, $ParseAllPossibleAtoms) { -//echo 'QuicktimeParseContainerAtom('.substr($atom_data, 4, 4).') @ '.$baseoffset.'

'; - $atom_structure = false; + $atom_structure = array(); $subatomoffset = 0; $subatomcounter = 0; if ((strlen($atom_data) == 4) && (getid3_lib::BigEndian2Int($atom_data) == 0x00000000)) { @@ -1762,18 +2158,31 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ $subatomoffset += 4; continue; } - return $atom_structure; + break; } - - $atom_structure[$subatomcounter] = $this->QuicktimeParseAtom($subatomname, $subatomsize, $subatomdata, $baseoffset + $subatomoffset, $atomHierarchy, $ParseAllPossibleAtoms); - + if (strlen($subatomdata) < ($subatomsize - 8)) { + // we don't have enough data to decode the subatom. + // this may be because we are refusing to parse large subatoms, or it may be because this atom had its size set too large + // so we passed in the start of a following atom incorrectly? + break; + } + $atom_structure[$subatomcounter++] = $this->QuicktimeParseAtom($subatomname, $subatomsize, $subatomdata, $baseoffset + $subatomoffset, $atomHierarchy, $ParseAllPossibleAtoms); $subatomoffset += $subatomsize; - $subatomcounter++; } + + if (empty($atom_structure)) { + return false; + } + return $atom_structure; } - + /** + * @param string $data + * @param int $offset + * + * @return int + */ public function quicktime_read_mp4_descr_length($data, &$offset) { // http://libquicktime.sourcearchive.com/documentation/2:1.0.2plus-pdebian-2build1/esds_8c-source.html $num_bytes = 0; @@ -1785,7 +2194,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return $length; } - + /** + * @param int $languageid + * + * @return string + */ public function QuicktimeLanguageLookup($languageid) { // http://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-34353 static $QuicktimeLanguageLookup = array(); @@ -1923,6 +2336,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeLanguageLookup[$languageid]) ? $QuicktimeLanguageLookup[$languageid] : 'invalid'); } + /** + * @param string $codecid + * + * @return string + */ public function QuicktimeVideoCodecLookup($codecid) { static $QuicktimeVideoCodecLookup = array(); if (empty($QuicktimeVideoCodecLookup)) { @@ -1951,6 +2369,7 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ $QuicktimeVideoCodecLookup['gif '] = 'GIF'; $QuicktimeVideoCodecLookup['h261'] = 'H261'; $QuicktimeVideoCodecLookup['h263'] = 'H263'; + $QuicktimeVideoCodecLookup['hvc1'] = 'H.265/HEVC'; $QuicktimeVideoCodecLookup['IV41'] = 'Indeo4'; $QuicktimeVideoCodecLookup['jpeg'] = 'JPEG'; $QuicktimeVideoCodecLookup['kpcd'] = 'PhotoCD'; @@ -1982,6 +2401,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeVideoCodecLookup[$codecid]) ? $QuicktimeVideoCodecLookup[$codecid] : ''); } + /** + * @param string $codecid + * + * @return mixed|string + */ public function QuicktimeAudioCodecLookup($codecid) { static $QuicktimeAudioCodecLookup = array(); if (empty($QuicktimeAudioCodecLookup)) { @@ -2027,6 +2451,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeAudioCodecLookup[$codecid]) ? $QuicktimeAudioCodecLookup[$codecid] : ''); } + /** + * @param string $compressionid + * + * @return string + */ public function QuicktimeDCOMLookup($compressionid) { static $QuicktimeDCOMLookup = array(); if (empty($QuicktimeDCOMLookup)) { @@ -2036,6 +2465,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeDCOMLookup[$compressionid]) ? $QuicktimeDCOMLookup[$compressionid] : ''); } + /** + * @param int $colordepthid + * + * @return string + */ public function QuicktimeColorNameLookup($colordepthid) { static $QuicktimeColorNameLookup = array(); if (empty($QuicktimeColorNameLookup)) { @@ -2054,6 +2488,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeColorNameLookup[$colordepthid]) ? $QuicktimeColorNameLookup[$colordepthid] : 'invalid'); } + /** + * @param int $stik + * + * @return string + */ public function QuicktimeSTIKLookup($stik) { static $QuicktimeSTIKLookup = array(); if (empty($QuicktimeSTIKLookup)) { @@ -2071,6 +2510,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeSTIKLookup[$stik]) ? $QuicktimeSTIKLookup[$stik] : 'invalid'); } + /** + * @param int $audio_profile_id + * + * @return string + */ public function QuicktimeIODSaudioProfileName($audio_profile_id) { static $QuicktimeIODSaudioProfileNameLookup = array(); if (empty($QuicktimeIODSaudioProfileNameLookup)) { @@ -2130,7 +2574,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeIODSaudioProfileNameLookup[$audio_profile_id]) ? $QuicktimeIODSaudioProfileNameLookup[$audio_profile_id] : 'ISO Reserved / User Private'); } - + /** + * @param int $video_profile_id + * + * @return string + */ public function QuicktimeIODSvideoProfileName($video_profile_id) { static $QuicktimeIODSvideoProfileNameLookup = array(); if (empty($QuicktimeIODSvideoProfileNameLookup)) { @@ -2202,17 +2650,27 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeIODSvideoProfileNameLookup[$video_profile_id]) ? $QuicktimeIODSvideoProfileNameLookup[$video_profile_id] : 'ISO Reserved Profile'); } - + /** + * @param int $rtng + * + * @return string + */ public function QuicktimeContentRatingLookup($rtng) { static $QuicktimeContentRatingLookup = array(); if (empty($QuicktimeContentRatingLookup)) { $QuicktimeContentRatingLookup[0] = 'None'; + $QuicktimeContentRatingLookup[1] = 'Explicit'; $QuicktimeContentRatingLookup[2] = 'Clean'; - $QuicktimeContentRatingLookup[4] = 'Explicit'; + $QuicktimeContentRatingLookup[4] = 'Explicit (old)'; } return (isset($QuicktimeContentRatingLookup[$rtng]) ? $QuicktimeContentRatingLookup[$rtng] : 'invalid'); } + /** + * @param int $akid + * + * @return string + */ public function QuicktimeStoreAccountTypeLookup($akid) { static $QuicktimeStoreAccountTypeLookup = array(); if (empty($QuicktimeStoreAccountTypeLookup)) { @@ -2222,6 +2680,11 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeStoreAccountTypeLookup[$akid]) ? $QuicktimeStoreAccountTypeLookup[$akid] : 'invalid'); } + /** + * @param int $sfid + * + * @return string + */ public function QuicktimeStoreFrontCodeLookup($sfid) { static $QuicktimeStoreFrontCodeLookup = array(); if (empty($QuicktimeStoreFrontCodeLookup)) { @@ -2251,185 +2714,13 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ return (isset($QuicktimeStoreFrontCodeLookup[$sfid]) ? $QuicktimeStoreFrontCodeLookup[$sfid] : 'invalid'); } - public function QuicktimeParseNikonNCTG($atom_data) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG - // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 - // Data is stored as records of: - // * 4 bytes record type - // * 2 bytes size of data field type: - // 0x0001 = flag (size field *= 1-byte) - // 0x0002 = char (size field *= 1-byte) - // 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB - // 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD - // 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together - // 0x0007 = bytes (size field *= 1-byte), values are stored as ?????? - // 0x0008 = ????? (size field *= 2-byte), values are stored as ?????? - // * 2 bytes data size field - // * ? bytes data (string data may be null-padded; datestamp fields are in the format "2011:05:25 20:24:15") - // all integers are stored BigEndian - - $NCTGtagName = array( - 0x00000001 => 'Make', - 0x00000002 => 'Model', - 0x00000003 => 'Software', - 0x00000011 => 'CreateDate', - 0x00000012 => 'DateTimeOriginal', - 0x00000013 => 'FrameCount', - 0x00000016 => 'FrameRate', - 0x00000022 => 'FrameWidth', - 0x00000023 => 'FrameHeight', - 0x00000032 => 'AudioChannels', - 0x00000033 => 'AudioBitsPerSample', - 0x00000034 => 'AudioSampleRate', - 0x02000001 => 'MakerNoteVersion', - 0x02000005 => 'WhiteBalance', - 0x0200000b => 'WhiteBalanceFineTune', - 0x0200001e => 'ColorSpace', - 0x02000023 => 'PictureControlData', - 0x02000024 => 'WorldTime', - 0x02000032 => 'UnknownInfo', - 0x02000083 => 'LensType', - 0x02000084 => 'Lens', - ); - - $offset = 0; - $datalength = strlen($atom_data); - $parsed = array(); - while ($offset < $datalength) { -//echo getid3_lib::PrintHexBytes(substr($atom_data, $offset, 4)).'
'; - $record_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); $offset += 4; - $data_size_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2; - $data_size = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2; - switch ($data_size_type) { - case 0x0001: // 0x0001 = flag (size field *= 1-byte) - $data = getid3_lib::BigEndian2Int(substr($atom_data, $offset, $data_size * 1)); - $offset += ($data_size * 1); - break; - case 0x0002: // 0x0002 = char (size field *= 1-byte) - $data = substr($atom_data, $offset, $data_size * 1); - $offset += ($data_size * 1); - $data = rtrim($data, "\x00"); - break; - case 0x0003: // 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB - $data = ''; - for ($i = $data_size - 1; $i >= 0; $i--) { - $data .= substr($atom_data, $offset + ($i * 2), 2); - } - $data = getid3_lib::BigEndian2Int($data); - $offset += ($data_size * 2); - break; - case 0x0004: // 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD - $data = ''; - for ($i = $data_size - 1; $i >= 0; $i--) { - $data .= substr($atom_data, $offset + ($i * 4), 4); - } - $data = getid3_lib::BigEndian2Int($data); - $offset += ($data_size * 4); - break; - case 0x0005: // 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together - $data = array(); - for ($i = 0; $i < $data_size; $i++) { - $numerator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 0, 4)); - $denomninator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 4, 4)); - if ($denomninator == 0) { - $data[$i] = false; - } else { - $data[$i] = (double) $numerator / $denomninator; - } - } - $offset += (8 * $data_size); - if (count($data) == 1) { - $data = $data[0]; - } - break; - case 0x0007: // 0x0007 = bytes (size field *= 1-byte), values are stored as ?????? - $data = substr($atom_data, $offset, $data_size * 1); - $offset += ($data_size * 1); - break; - case 0x0008: // 0x0008 = ????? (size field *= 2-byte), values are stored as ?????? - $data = substr($atom_data, $offset, $data_size * 2); - $offset += ($data_size * 2); - break; - default: -echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
'; - break 2; - } - - switch ($record_type) { - case 0x00000011: // CreateDate - case 0x00000012: // DateTimeOriginal - $data = strtotime($data); - break; - case 0x0200001e: // ColorSpace - switch ($data) { - case 1: - $data = 'sRGB'; - break; - case 2: - $data = 'Adobe RGB'; - break; - } - break; - case 0x02000023: // PictureControlData - $PictureControlAdjust = array(0=>'default', 1=>'quick', 2=>'full'); - $FilterEffect = array(0x80=>'off', 0x81=>'yellow', 0x82=>'orange', 0x83=>'red', 0x84=>'green', 0xff=>'n/a'); - $ToningEffect = array(0x80=>'b&w', 0x81=>'sepia', 0x82=>'cyanotype', 0x83=>'red', 0x84=>'yellow', 0x85=>'green', 0x86=>'blue-green', 0x87=>'blue', 0x88=>'purple-blue', 0x89=>'red-purple', 0xff=>'n/a'); - $data = array( - 'PictureControlVersion' => substr($data, 0, 4), - 'PictureControlName' => rtrim(substr($data, 4, 20), "\x00"), - 'PictureControlBase' => rtrim(substr($data, 24, 20), "\x00"), - //'?' => substr($data, 44, 4), - 'PictureControlAdjust' => $PictureControlAdjust[ord(substr($data, 48, 1))], - 'PictureControlQuickAdjust' => ord(substr($data, 49, 1)), - 'Sharpness' => ord(substr($data, 50, 1)), - 'Contrast' => ord(substr($data, 51, 1)), - 'Brightness' => ord(substr($data, 52, 1)), - 'Saturation' => ord(substr($data, 53, 1)), - 'HueAdjustment' => ord(substr($data, 54, 1)), - 'FilterEffect' => $FilterEffect[ord(substr($data, 55, 1))], - 'ToningEffect' => $ToningEffect[ord(substr($data, 56, 1))], - 'ToningSaturation' => ord(substr($data, 57, 1)), - ); - break; - case 0x02000024: // WorldTime - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#WorldTime - // timezone is stored as offset from GMT in minutes - $timezone = getid3_lib::BigEndian2Int(substr($data, 0, 2)); - if ($timezone & 0x8000) { - $timezone = 0 - (0x10000 - $timezone); - } - $timezone /= 60; - - $dst = (bool) getid3_lib::BigEndian2Int(substr($data, 2, 1)); - switch (getid3_lib::BigEndian2Int(substr($data, 3, 1))) { - case 2: - $datedisplayformat = 'D/M/Y'; break; - case 1: - $datedisplayformat = 'M/D/Y'; break; - case 0: - default: - $datedisplayformat = 'Y/M/D'; break; - } - - $data = array('timezone'=>floatval($timezone), 'dst'=>$dst, 'display'=>$datedisplayformat); - break; - case 0x02000083: // LensType - $data = array( - //'_' => $data, - 'mf' => (bool) ($data & 0x01), - 'd' => (bool) ($data & 0x02), - 'g' => (bool) ($data & 0x04), - 'vr' => (bool) ($data & 0x08), - ); - break; - } - $tag_name = (isset($NCTGtagName[$record_type]) ? $NCTGtagName[$record_type] : '0x'.str_pad(dechex($record_type), 8, '0', STR_PAD_LEFT)); - $parsed[$tag_name] = $data; - } - return $parsed; - } - - + /** + * @param string $keyname + * @param string|array $data + * @param string $boxname + * + * @return bool + */ public function CopyToAppropriateCommentsSection($keyname, $data, $boxname='') { static $handyatomtranslatorarray = array(); if (empty($handyatomtranslatorarray)) { @@ -2473,7 +2764,7 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
getid3->info; @@ -2539,19 +2830,8 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
$data, 'image_mime'=>$image_mime); - } + // already copied directly into [comments][picture] elsewhere, do not re-copy here + return true; } $gooddata = array($data); if ($comment_key == 'genre') { @@ -2559,45 +2839,57 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
2 && (($lstring[0] == "\xFE" && $lstring[1] == "\xFF") || ($lstring[0] == "\xFF" && $lstring[1] == "\xFE"))) { - //UTF-16 - if (preg_match('/(.*)\x00/', $lstring, $lmatches)){ - $count = strlen($lmatches[1]) * 2 + 2; //account for 2 byte characters and trailing \x0000 - return getid3_lib::iconv_fallback_utf16_utf8($lmatches[1]); - } else { - return ''; - } - } else { - //UTF-8 - if (preg_match('/(.*)\x00/', $lstring, $lmatches)){ - $count = strlen($lmatches[1]) + 1; //account for trailing \x00 - return $lmatches[1]; - }else { - return ''; - } - - } - } + /** + * @param string $lstring + * @param int $count + * + * @return string + */ + public function LociString($lstring, &$count) { + // Loci strings are UTF-8 or UTF-16 and null (x00/x0000) terminated. UTF-16 has a BOM + // Also need to return the number of bytes the string occupied so additional fields can be extracted + $len = strlen($lstring); + if ($len == 0) { + $count = 0; + return ''; + } + if ($lstring[0] == "\x00") { + $count = 1; + return ''; + } + // check for BOM + if (($len > 2) && ((($lstring[0] == "\xFE") && ($lstring[1] == "\xFF")) || (($lstring[0] == "\xFF") && ($lstring[1] == "\xFE")))) { + // UTF-16 + if (preg_match('/(.*)\x00/', $lstring, $lmatches)) { + $count = strlen($lmatches[1]) * 2 + 2; //account for 2 byte characters and trailing \x0000 + return getid3_lib::iconv_fallback_utf16_utf8($lmatches[1]); + } else { + return ''; + } + } + // UTF-8 + if (preg_match('/(.*)\x00/', $lstring, $lmatches)) { + $count = strlen($lmatches[1]) + 1; //account for trailing \x00 + return $lmatches[1]; + } + return ''; + } + /** + * @param string $nullterminatedstring + * + * @return string + */ public function NoNullString($nullterminatedstring) { // remove the single null terminator on null terminated strings if (substr($nullterminatedstring, strlen($nullterminatedstring) - 1, 1) === "\x00") { @@ -2606,16 +2898,43 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
$value) { $key_history = $history.'/'.$key; @@ -2629,6 +2948,13 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
$value) { $key_history = $history.'/'.$key; @@ -2642,6 +2968,11 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
search_tag_by_pair($info['quicktime']['moov'], 'name', 'stbl', 'quicktime/moov', $res); @@ -2659,7 +2990,12 @@ echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
0) { $ts_res = array(); $this->search_tag_by_key($info['quicktime']['moov'], 'time_scale', 'quicktime/moov', $ts_res); - foreach ($ts_res as $value) { - $prefix = substr($value[0], 0, -12); + foreach ($ts_res as $sub_value) { + $prefix = substr($sub_value[0], 0, -12); if ((substr($stbl_res[0][0], 0, strlen($prefix)) === $prefix) && ($ts_prefix_len < strlen($prefix))) { - $time_scale = $value[1]['time_scale']; + $time_scale = $sub_value[1]['time_scale']; $ts_prefix_len = strlen($prefix); } } diff --git a/lib/getid3/module.audio-video.real.php b/lib/getid3/module.audio-video.real.php index 280d43c..188df62 100644 --- a/lib/getid3/module.audio-video.real.php +++ b/lib/getid3/module.audio-video.real.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.real.php // @@ -14,11 +14,16 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); class getid3_real extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -42,8 +47,13 @@ class getid3_real extends getid3_handler $info['audio']['bits_per_sample'] = $info['real']['old_ra_header']['bits_per_sample']; $info['audio']['channels'] = $info['real']['old_ra_header']['channels']; - $info['playtime_seconds'] = 60 * ($info['real']['old_ra_header']['audio_bytes'] / $info['real']['old_ra_header']['bytes_per_minute']); - $info['audio']['bitrate'] = 8 * ($info['real']['old_ra_header']['audio_bytes'] / $info['playtime_seconds']); + if ($info['real']['old_ra_header']['bytes_per_minute']) { + $info['playtime_seconds'] = 60 * ($info['real']['old_ra_header']['audio_bytes'] / $info['real']['old_ra_header']['bytes_per_minute']); + $info['audio']['bitrate'] = 8 * ($info['real']['old_ra_header']['audio_bytes'] / $info['playtime_seconds']); + } else { + $info['playtime_seconds'] = 0; + $info['audio']['bitrate'] = 0; + } $info['audio']['codec'] = $this->RealAudioCodecFourCClookup($info['real']['old_ra_header']['fourcc'], $info['audio']['bitrate']); foreach ($info['real']['old_ra_header']['comments'] as $key => $valuearray) { @@ -214,7 +224,8 @@ class getid3_real extends getid3_handler case 'audio/x-pn-realaudio': case 'audio/x-pn-multirate-realaudio': - $this->ParseOldRAheader($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk['parsed_audio_data']); + $this->ParseOldRAheader($thisfile_real_chunks_currentchunk_typespecificdata, $parsedAudioData); + $thisfile_real_chunks_currentchunk['parsed_audio_data'] = &$parsedAudioData; $info['audio']['sample_rate'] = $thisfile_real_chunks_currentchunk['parsed_audio_data']['sample_rate']; $info['audio']['bits_per_sample'] = $thisfile_real_chunks_currentchunk['parsed_audio_data']['bits_per_sample']; @@ -370,7 +381,12 @@ class getid3_real extends getid3_handler return true; } - + /** + * @param string $OldRAheaderData + * @param array $ParsedArray + * + * @return bool + */ public function ParseOldRAheader($OldRAheaderData, &$ParsedArray) { // http://www.freelists.org/archives/matroska-devel/07-2003/msg00010.html @@ -476,8 +492,9 @@ class getid3_real extends getid3_handler $ParsedArray['fourcc'] = $ParsedArray['fourcc3']; } + /** @var string[]|false[] $value */ foreach ($ParsedArray['comments'] as $key => $value) { - if ($ParsedArray['comments'][$key][0] === false) { + if ($value[0] === false) { $ParsedArray['comments'][$key][0] = ''; } } @@ -485,6 +502,12 @@ class getid3_real extends getid3_handler return true; } + /** + * @param string $fourcc + * @param int $bitrate + * + * @return string + */ public function RealAudioCodecFourCClookup($fourcc, $bitrate) { static $RealAudioCodecFourCClookup = array(); if (empty($RealAudioCodecFourCClookup)) { diff --git a/lib/getid3/module.audio-video.riff.php b/lib/getid3/module.audio-video.riff.php index fcf965c..3dc18de 100644 --- a/lib/getid3/module.audio-video.riff.php +++ b/lib/getid3/module.audio-video.riff.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.riff.php // @@ -23,14 +23,22 @@ * @todo Rewrite RIFF parser totally */ +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ac3.php', __FILE__, true); getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.dts.php', __FILE__, true); -class getid3_riff extends getid3_handler { - +class getid3_riff extends getid3_handler +{ protected $container = 'riff'; // default + /** + * @return bool + * + * @throws getid3_exception + */ public function Analyze() { $info = &$this->getid3->info; @@ -46,7 +54,9 @@ class getid3_riff extends getid3_handler { $thisfile_audio_dataformat = &$thisfile_audio['dataformat']; $thisfile_riff_audio = &$thisfile_riff['audio']; $thisfile_riff_video = &$thisfile_riff['video']; + $thisfile_riff_WAVE = array(); + $Original = array(); $Original['avdataoffset'] = $info['avdataoffset']; $Original['avdataend'] = $info['avdataend']; @@ -197,14 +207,14 @@ class getid3_riff extends getid3_handler { unset($thisfile_riff_audio[$streamindex]['raw']); $thisfile_audio['streams'][$streamindex] = $thisfile_riff_audio[$streamindex]; - $thisfile_audio = getid3_lib::array_merge_noclobber($thisfile_audio, $thisfile_riff_audio[$streamindex]); + $thisfile_audio = (array) getid3_lib::array_merge_noclobber($thisfile_audio, $thisfile_riff_audio[$streamindex]); if (substr($thisfile_audio['codec'], 0, strlen('unknown: 0x')) == 'unknown: 0x') { $this->warning('Audio codec = '.$thisfile_audio['codec']); } $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; if (empty($info['playtime_seconds'])) { // may already be set (e.g. DTS-WAV) - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); + $info['playtime_seconds'] = (float)getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $thisfile_audio['bitrate']); } $thisfile_audio['lossless'] = false; @@ -287,9 +297,18 @@ class getid3_riff extends getid3_handler { // shortcut $thisfile_riff_WAVE_bext_0 = &$thisfile_riff_WAVE['bext'][0]; - $thisfile_riff_WAVE_bext_0['title'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 0, 256)); - $thisfile_riff_WAVE_bext_0['author'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 256, 32)); - $thisfile_riff_WAVE_bext_0['reference'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 288, 32)); + $thisfile_riff_WAVE_bext_0['title'] = substr($thisfile_riff_WAVE_bext_0['data'], 0, 256); + $thisfile_riff_WAVE_bext_0['author'] = substr($thisfile_riff_WAVE_bext_0['data'], 256, 32); + $thisfile_riff_WAVE_bext_0['reference'] = substr($thisfile_riff_WAVE_bext_0['data'], 288, 32); + foreach (array('title','author','reference') as $bext_key) { + // Some software (notably Logic Pro) may not blank existing data before writing a null-terminated string to the offsets + // assigned for text fields, resulting in a null-terminated string (or possibly just a single null) followed by garbage + // Keep only string as far as first null byte, discard rest of fixed-width data + // https://github.com/JamesHeinrich/getID3/issues/263 + $null_terminator_offset = strpos($thisfile_riff_WAVE_bext_0[$bext_key], "\x00"); + $thisfile_riff_WAVE_bext_0[$bext_key] = substr($thisfile_riff_WAVE_bext_0[$bext_key], 0, $null_terminator_offset); + } + $thisfile_riff_WAVE_bext_0['origin_date'] = substr($thisfile_riff_WAVE_bext_0['data'], 320, 10); $thisfile_riff_WAVE_bext_0['origin_time'] = substr($thisfile_riff_WAVE_bext_0['data'], 330, 8); $thisfile_riff_WAVE_bext_0['time_reference'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_bext_0['data'], 338, 8)); @@ -298,6 +317,7 @@ class getid3_riff extends getid3_handler { $thisfile_riff_WAVE_bext_0['coding_history'] = explode("\r\n", trim(substr($thisfile_riff_WAVE_bext_0['data'], 601))); if (preg_match('#^([0-9]{4}).([0-9]{2}).([0-9]{2})$#', $thisfile_riff_WAVE_bext_0['origin_date'], $matches_bext_date)) { if (preg_match('#^([0-9]{2}).([0-9]{2}).([0-9]{2})$#', $thisfile_riff_WAVE_bext_0['origin_time'], $matches_bext_time)) { + $bext_timestamp = array(); list($dummy, $bext_timestamp['year'], $bext_timestamp['month'], $bext_timestamp['day']) = $matches_bext_date; list($dummy, $bext_timestamp['hour'], $bext_timestamp['minute'], $bext_timestamp['second']) = $matches_bext_time; $thisfile_riff_WAVE_bext_0['origin_date_unix'] = gmmktime($bext_timestamp['hour'], $bext_timestamp['minute'], $bext_timestamp['second'], $bext_timestamp['month'], $bext_timestamp['day'], $bext_timestamp['year']); @@ -406,7 +426,6 @@ class getid3_riff extends getid3_handler { 'tracktitle'=>'title', 'category' =>'genre', 'cdtitle' =>'album', - 'tracktitle'=>'title', ); foreach ($tagmapping as $fromkey => $tokey) { if (isset($thisfile_riff_WAVE_SNDM_0['parsed'][$fromkey])) { @@ -421,11 +440,11 @@ class getid3_riff extends getid3_handler { $thisfile_riff_WAVE['iXML'][0]['parsed'] = $parsedXML; if (isset($parsedXML['SPEED']['MASTER_SPEED'])) { @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['MASTER_SPEED']); - $thisfile_riff_WAVE['iXML'][0]['master_speed'] = $numerator / ($denominator ? $denominator : 1000); + $thisfile_riff_WAVE['iXML'][0]['master_speed'] = (int) $numerator / ($denominator ? $denominator : 1000); } if (isset($parsedXML['SPEED']['TIMECODE_RATE'])) { @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['TIMECODE_RATE']); - $thisfile_riff_WAVE['iXML'][0]['timecode_rate'] = $numerator / ($denominator ? $denominator : 1000); + $thisfile_riff_WAVE['iXML'][0]['timecode_rate'] = (int) $numerator / ($denominator ? $denominator : 1000); } if (isset($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO']) && !empty($parsedXML['SPEED']['TIMESTAMP_SAMPLE_RATE']) && !empty($thisfile_riff_WAVE['iXML'][0]['timecode_rate'])) { $samples_since_midnight = floatval(ltrim($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_HI'].$parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO'], '0')); @@ -443,11 +462,66 @@ class getid3_riff extends getid3_handler { } } + if (isset($thisfile_riff_WAVE['guan'][0]['data'])) { + // shortcut + $thisfile_riff_WAVE_guan_0 = &$thisfile_riff_WAVE['guan'][0]; + if (!empty($thisfile_riff_WAVE_guan_0['data']) && (substr($thisfile_riff_WAVE_guan_0['data'], 0, 14) == 'GUANO|Version:')) { + $thisfile_riff['guano'] = array(); + foreach (explode("\n", $thisfile_riff_WAVE_guan_0['data']) as $line) { + if ($line) { + @list($key, $value) = explode(':', $line, 2); + if (substr($value, 0, 3) == '[{"') { + if ($decoded = @json_decode($value, true)) { + if (!empty($decoded) && (count($decoded) == 1)) { + $value = $decoded[0]; + } else { + $value = $decoded; + } + } + } + $thisfile_riff['guano'] = array_merge_recursive($thisfile_riff['guano'], getid3_lib::CreateDeepArray($key, '|', $value)); + } + } + // https://www.wildlifeacoustics.com/SCHEMA/GUANO.html + foreach ($thisfile_riff['guano'] as $key => $value) { + switch ($key) { + case 'Loc Position': + if (preg_match('#^([\\+\\-]?[0-9]+\\.[0-9]+) ([\\+\\-]?[0-9]+\\.[0-9]+)$#', $value, $matches)) { + list($dummy, $latitude, $longitude) = $matches; + $thisfile_riff['comments']['gps_latitude'][0] = floatval($latitude); + $thisfile_riff['comments']['gps_longitude'][0] = floatval($longitude); + $thisfile_riff['guano'][$key] = floatval($latitude).' '.floatval($longitude); + } + break; + case 'Loc Elevation': // Elevation/altitude above mean sea level in meters + $thisfile_riff['comments']['gps_altitude'][0] = floatval($value); + $thisfile_riff['guano'][$key] = (float) $value; + break; + case 'Filter HP': // High-pass filter frequency in kHz + case 'Filter LP': // Low-pass filter frequency in kHz + case 'Humidity': // Relative humidity as a percentage + case 'Length': // Recording length in seconds + case 'Loc Accuracy': // Estimated Position Error in meters + case 'Temperature Ext': // External temperature in degrees Celsius outside the recorder's housing + case 'Temperature Int': // Internal temperature in degrees Celsius inside the recorder's housing + $thisfile_riff['guano'][$key] = (float) $value; + break; + case 'Samplerate': // Recording sample rate, Hz + case 'TE': // Time-expansion factor. If not specified, then 1 (no time-expansion a.k.a. direct-recording) is assumed. + $thisfile_riff['guano'][$key] = (int) $value; + break; + } + } + + } else { + $this->warning('RIFF.guan data not in expected format'); + } + } if (!isset($thisfile_audio['bitrate']) && isset($thisfile_riff_audio[$streamindex]['bitrate'])) { $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); + $info['playtime_seconds'] = (float)getid3_lib::SafeDiv((($info['avdataend'] - $info['avdataoffset']) * 8), $thisfile_audio['bitrate']); } if (!empty($info['wavpack'])) { @@ -457,7 +531,7 @@ class getid3_riff extends getid3_handler { // Reset to the way it was - RIFF parsing will have messed this up $info['avdataend'] = $Original['avdataend']; - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $thisfile_audio['bitrate'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); $this->fseek($info['avdataoffset'] - 44); $RIFFdata = $this->fread(44); @@ -558,7 +632,7 @@ class getid3_riff extends getid3_handler { } } if ($info['avdataend'] > $info['filesize']) { - switch (!empty($thisfile_audio_dataformat) ? $thisfile_audio_dataformat : '') { + switch ($thisfile_audio_dataformat) { case 'wavpack': // WavPack case 'lpac': // LPAC case 'ofr': // OptimFROG @@ -598,7 +672,7 @@ class getid3_riff extends getid3_handler { $this->warning('Extra null byte at end of MP3 data assumed to be RIFF padding and therefore ignored'); } } - if (isset($thisfile_audio_dataformat) && ($thisfile_audio_dataformat == 'ac3')) { + if ($thisfile_audio_dataformat == 'ac3') { unset($thisfile_audio['bits_per_sample']); if (!empty($info['ac3']['bitrate']) && ($info['ac3']['bitrate'] != $thisfile_audio['bitrate'])) { $thisfile_audio['bitrate'] = $info['ac3']['bitrate']; @@ -614,6 +688,8 @@ class getid3_riff extends getid3_handler { $thisfile_video['bitrate_mode'] = 'vbr'; // maybe not, but probably $thisfile_video['dataformat'] = 'avi'; + $thisfile_riff_video_current = array(); + if (isset($thisfile_riff[$RIFFsubtype]['movi']['offset'])) { $info['avdataoffset'] = $thisfile_riff[$RIFFsubtype]['movi']['offset'] + 8; if (isset($thisfile_riff['AVIX'])) { @@ -696,23 +772,24 @@ class getid3_riff extends getid3_handler { 'capturedfile' => 0x00010000, 'copyrighted' => 0x00020010, ); - foreach ($flags as $flag => $value) { + foreach ($flags as $flag => $value) { $thisfile_riff_raw_avih['flags'][$flag] = (bool) ($thisfile_riff_raw_avih['dwFlags'] & $value); } // shortcut $thisfile_riff_video[$streamindex] = array(); + /** @var array $thisfile_riff_video_current */ $thisfile_riff_video_current = &$thisfile_riff_video[$streamindex]; - if ($thisfile_riff_raw_avih['dwWidth'] > 0) { + if ($thisfile_riff_raw_avih['dwWidth'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['frame_width'] = $thisfile_riff_raw_avih['dwWidth']; $thisfile_video['resolution_x'] = $thisfile_riff_video_current['frame_width']; } - if ($thisfile_riff_raw_avih['dwHeight'] > 0) { + if ($thisfile_riff_raw_avih['dwHeight'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['frame_height'] = $thisfile_riff_raw_avih['dwHeight']; $thisfile_video['resolution_y'] = $thisfile_riff_video_current['frame_height']; } - if ($thisfile_riff_raw_avih['dwTotalFrames'] > 0) { + if ($thisfile_riff_raw_avih['dwTotalFrames'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['total_frames'] = $thisfile_riff_raw_avih['dwTotalFrames']; $thisfile_video['total_frames'] = $thisfile_riff_video_current['total_frames']; } @@ -722,6 +799,7 @@ class getid3_riff extends getid3_handler { } if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strh'][0]['data'])) { if (is_array($thisfile_riff['AVI ']['hdrl']['strl']['strh'])) { + $thisfile_riff_raw_strf_strhfccType_streamindex = null; for ($i = 0; $i < count($thisfile_riff['AVI ']['hdrl']['strl']['strh']); $i++) { if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strh'][$i]['data'])) { $strhData = $thisfile_riff['AVI ']['hdrl']['strl']['strh'][$i]['data']; @@ -730,6 +808,9 @@ class getid3_riff extends getid3_handler { if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strf'][$i]['data'])) { $strfData = $thisfile_riff['AVI ']['hdrl']['strl']['strf'][$i]['data']; + if (!isset($thisfile_riff_raw['strf'][$strhfccType][$streamindex])) { + $thisfile_riff_raw['strf'][$strhfccType][$streamindex] = null; + } // shortcut $thisfile_riff_raw_strf_strhfccType_streamindex = &$thisfile_riff_raw['strf'][$strhfccType][$streamindex]; @@ -868,7 +949,7 @@ class getid3_riff extends getid3_handler { } } - if (isset($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc'])) { + if (isset($thisfile_riff_raw_strf_strhfccType_streamindex) && isset($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc'])) { $thisfile_video['fourcc'] = $thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']; if (self::fourccLookup($thisfile_video['fourcc'])) { @@ -915,7 +996,7 @@ class getid3_riff extends getid3_handler { // http://en.wikipedia.org/wiki/CD-DA case 'CDDA': $info['fileformat'] = 'cda'; - unset($info['mime_type']); + unset($info['mime_type']); $thisfile_audio_dataformat = 'cda'; @@ -935,7 +1016,7 @@ class getid3_riff extends getid3_handler { $thisfile_riff_CDDA_fmt_0['start_offset_seconds'] = (float) $thisfile_riff_CDDA_fmt_0['start_offset_frame'] / 75; $thisfile_riff_CDDA_fmt_0['playtime_seconds'] = (float) $thisfile_riff_CDDA_fmt_0['playtime_frames'] / 75; - $info['comments']['track'] = $thisfile_riff_CDDA_fmt_0['track_num']; + $info['comments']['track_number'] = $thisfile_riff_CDDA_fmt_0['track_num']; $info['playtime_seconds'] = $thisfile_riff_CDDA_fmt_0['playtime_seconds']; // hardcoded data for CD-audio @@ -948,7 +1029,7 @@ class getid3_riff extends getid3_handler { } break; - // http://en.wikipedia.org/wiki/AIFF + // http://en.wikipedia.org/wiki/AIFF case 'AIFF': case 'AIFC': $info['fileformat'] = 'aiff'; @@ -1058,7 +1139,7 @@ class getid3_riff extends getid3_handler { if (isset($thisfile_riff[$RIFFsubtype]['ID3 '])) { getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_id3v2 = new getid3_id3v2($getid3_temp); $getid3_id3v2->StartingOffset = $thisfile_riff[$RIFFsubtype]['ID3 '][0]['offset'] + 8; if ($thisfile_riff[$RIFFsubtype]['ID3 '][0]['valid'] = $getid3_id3v2->Analyze()) { @@ -1078,6 +1159,7 @@ class getid3_riff extends getid3_handler { $thisfile_audio_dataformat = '8svx'; $thisfile_audio['bits_per_sample'] = 8; $thisfile_audio['channels'] = 1; // overridden below, if need be + $ActualBitsPerSample = 0; if (isset($thisfile_riff[$RIFFsubtype]['BODY'][0]['offset'])) { $info['avdataoffset'] = $thisfile_riff[$RIFFsubtype]['BODY'][0]['offset'] + 8; @@ -1115,7 +1197,7 @@ class getid3_riff extends getid3_handler { break; default: - $this->warning('Unexpected sCompression value in 8SVX.VHDR chunk - expecting 0 or 1, found "'.sCompression.'"'); + $this->warning('Unexpected sCompression value in 8SVX.VHDR chunk - expecting 0 or 1, found "'.$thisfile_riff_RIFFsubtype_VHDR_0['sCompression'].'"'); break; } } @@ -1160,7 +1242,7 @@ class getid3_riff extends getid3_handler { getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.mpeg.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_mpeg = new getid3_mpeg($getid3_temp); $getid3_mpeg->Analyze(); if (empty($getid3_temp->info['error'])) { @@ -1246,7 +1328,7 @@ class getid3_riff extends getid3_handler { getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_id3v2 = new getid3_id3v2($getid3_temp); $getid3_id3v2->StartingOffset = $thisfile_riff[$RIFFsubtype]['id3 '][0]['offset'] + 8; if ($thisfile_riff[$RIFFsubtype]['id3 '][0]['valid'] = $getid3_id3v2->Analyze()) { @@ -1274,10 +1356,10 @@ class getid3_riff extends getid3_handler { if (!isset($info['playtime_seconds'])) { $info['playtime_seconds'] = 0; } - if (isset($thisfile_riff_raw['strh'][0]['dwLength']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { + if (isset($thisfile_riff_raw['strh'][0]['dwLength']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { // @phpstan-ignore-line // needed for >2GB AVIs where 'avih' chunk only lists number of frames in that chunk, not entire movie $info['playtime_seconds'] = $thisfile_riff_raw['strh'][0]['dwLength'] * ($thisfile_riff_raw['avih']['dwMicroSecPerFrame'] / 1000000); - } elseif (isset($thisfile_riff_raw['avih']['dwTotalFrames']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { + } elseif (isset($thisfile_riff_raw['avih']['dwTotalFrames']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { // @phpstan-ignore-line $info['playtime_seconds'] = $thisfile_riff_raw['avih']['dwTotalFrames'] * ($thisfile_riff_raw['avih']['dwMicroSecPerFrame'] / 1000000); } @@ -1373,6 +1455,15 @@ class getid3_riff extends getid3_handler { return true; } + /** + * @param int $startoffset + * @param int $maxoffset + * + * @return array|false + * + * @throws Exception + * @throws getid3_exception + */ public function ParseRIFFAMV($startoffset, $maxoffset) { // AMV files are RIFF-AVI files with parts of the spec deliberately broken, such as chunk size fields hardcoded to zero (because players known in hardware that these fields are always a certain size @@ -1481,12 +1572,21 @@ class getid3_riff extends getid3_handler { return $RIFFchunk; } - + /** + * @param int $startoffset + * @param int $maxoffset + * + * @return array|false + * @throws getid3_exception + */ public function ParseRIFF($startoffset, $maxoffset) { $info = &$this->getid3->info; - $RIFFchunk = false; + $RIFFchunk = array(); $FoundAllChunksWeNeed = false; + $LISTchunkParent = null; + $LISTchunkMaxOffset = null; + $AC3syncwordBytes = pack('n', getid3_ac3::syncword); // 0x0B77 -> "\x0B\x77" try { $this->fseek($startoffset); @@ -1530,7 +1630,7 @@ class getid3_riff extends getid3_handler { // MP3 if (getid3_mp3::MPEGaudioHeaderBytesValid($FirstFourBytes)) { $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $this->ftell() - 4; $getid3_temp->info['avdataend'] = $this->ftell() + $AudioChunkSize; $getid3_mp3 = new getid3_mp3($getid3_temp, __CLASS__); @@ -1548,11 +1648,10 @@ class getid3_riff extends getid3_handler { unset($getid3_temp, $getid3_mp3); } - } elseif (strpos($FirstFourBytes, getid3_ac3::syncword) === 0) { - + } elseif (strpos($FirstFourBytes, $AC3syncwordBytes) === 0) { // AC3 $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $this->ftell() - 4; $getid3_temp->info['avdataend'] = $this->ftell() + $AudioChunkSize; $getid3_ac3 = new getid3_ac3($getid3_temp); @@ -1613,7 +1712,7 @@ class getid3_riff extends getid3_handler { // Probably is MP3 data if (getid3_mp3::MPEGaudioHeaderBytesValid(substr($testData, 0, 4))) { $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; $getid3_mp3 = new getid3_mp3($getid3_temp, __CLASS__); @@ -1625,12 +1724,12 @@ class getid3_riff extends getid3_handler { unset($getid3_temp, $getid3_mp3); } - } elseif (($isRegularAC3 = (substr($testData, 0, 2) == getid3_ac3::syncword)) || substr($testData, 8, 2) == strrev(getid3_ac3::syncword)) { + } elseif (($isRegularAC3 = (substr($testData, 0, 2) == $AC3syncwordBytes)) || substr($testData, 8, 2) == strrev($AC3syncwordBytes)) { // This is probably AC-3 data $getid3_temp = new getID3(); if ($isRegularAC3) { - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; } @@ -1646,6 +1745,8 @@ class getid3_riff extends getid3_handler { $ac3_data .= substr($testData, 8 + $i + 1, 1); $ac3_data .= substr($testData, 8 + $i + 0, 1); } + $getid3_ac3->getid3->info['avdataoffset'] = 0; + $getid3_ac3->getid3->info['avdataend'] = strlen($ac3_data); $getid3_ac3->AnalyzeString($ac3_data); } @@ -1664,7 +1765,7 @@ class getid3_riff extends getid3_handler { // This is probably DTS data $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_dts = new getid3_dts($getid3_temp); $getid3_dts->Analyze(); @@ -1705,6 +1806,8 @@ class getid3_riff extends getid3_handler { case 'indx': case 'MEXT': case 'DISP': + case 'wamd': + case 'guan': // always read data in case 'JUNK': // should be: never read data in @@ -1732,8 +1835,77 @@ class getid3_riff extends getid3_handler { // $info['divxtag']['comments'] = self::ParseDIVXTAG($this->fread($chunksize)); // break; + case 'scot': + // https://cmsdk.com/node-js/adding-scot-chunk-to-wav-file.html + $RIFFchunk[$chunkname][$thisindex]['data'] = $this->fread($chunksize); + $RIFFchunk[$chunkname][$thisindex]['parsed']['alter'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 0, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['attrib'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 1, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['artnum'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 2, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['title'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 4, 43); // "name" in other documentation + $RIFFchunk[$chunkname][$thisindex]['parsed']['copy'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 47, 4); + $RIFFchunk[$chunkname][$thisindex]['parsed']['padd'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 51, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['asclen'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 52, 5); + $RIFFchunk[$chunkname][$thisindex]['parsed']['startseconds'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 57, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['starthundredths'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 59, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['endseconds'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 61, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['endhundreths'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 63, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['sdate'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 65, 6); + $RIFFchunk[$chunkname][$thisindex]['parsed']['kdate'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 71, 6); + $RIFFchunk[$chunkname][$thisindex]['parsed']['start_hr'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 77, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['kill_hr'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 78, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['digital'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 79, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 80, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['stereo'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 82, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['compress'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 83, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['eomstrt'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 84, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['eomlen'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 88, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['attrib2'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 90, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['future1'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 94, 12); + $RIFFchunk[$chunkname][$thisindex]['parsed']['catfontcolor'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 106, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['catcolor'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 110, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['segeompos'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 114, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['vt_startsecs'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 118, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['vt_starthunds'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 120, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['priorcat'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 122, 3); + $RIFFchunk[$chunkname][$thisindex]['parsed']['priorcopy'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 125, 4); + $RIFFchunk[$chunkname][$thisindex]['parsed']['priorpadd'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 129, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['postcat'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 130, 3); + $RIFFchunk[$chunkname][$thisindex]['parsed']['postcopy'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 133, 4); + $RIFFchunk[$chunkname][$thisindex]['parsed']['postpadd'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 137, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['hrcanplay'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 138, 21); + $RIFFchunk[$chunkname][$thisindex]['parsed']['future2'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 159, 108); + $RIFFchunk[$chunkname][$thisindex]['parsed']['artist'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 267, 34); + $RIFFchunk[$chunkname][$thisindex]['parsed']['comment'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 301, 34); // "trivia" in other documentation + $RIFFchunk[$chunkname][$thisindex]['parsed']['intro'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 335, 2); + $RIFFchunk[$chunkname][$thisindex]['parsed']['end'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 337, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['year'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 338, 4); + $RIFFchunk[$chunkname][$thisindex]['parsed']['obsolete2'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 342, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['rec_hr'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 343, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['rdate'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 344, 6); + $RIFFchunk[$chunkname][$thisindex]['parsed']['mpeg_bitrate'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 350, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['pitch'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 352, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['playlevel'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 354, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['lenvalid'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 356, 1); + $RIFFchunk[$chunkname][$thisindex]['parsed']['filelength'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 357, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['newplaylevel'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 361, 2)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['chopsize'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 363, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['vteomovr'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 367, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['desiredlen'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 371, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['triggers'] = getid3_lib::LittleEndian2Int(substr($RIFFchunk[$chunkname][$thisindex]['data'], 375, 4)); + $RIFFchunk[$chunkname][$thisindex]['parsed']['fillout'] = substr($RIFFchunk[$chunkname][$thisindex]['data'], 379, 33); + + foreach (array('title', 'artist', 'comment') as $key) { + if (trim($RIFFchunk[$chunkname][$thisindex]['parsed'][$key])) { + $info['riff']['comments'][$key] = array($RIFFchunk[$chunkname][$thisindex]['parsed'][$key]); + } + } + if ($RIFFchunk[$chunkname][$thisindex]['parsed']['filelength'] && !empty($info['filesize']) && ($RIFFchunk[$chunkname][$thisindex]['parsed']['filelength'] != $info['filesize'])) { + $this->warning('RIFF.WAVE.scot.filelength ('.$RIFFchunk[$chunkname][$thisindex]['parsed']['filelength'].') different from actual filesize ('.$info['filesize'].')'); + } + break; + default: - if (!empty($LISTchunkParent) && (($RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size']) <= $LISTchunkMaxOffset)) { + if (!empty($LISTchunkParent) && isset($LISTchunkMaxOffset) && (($RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size']) <= $LISTchunkMaxOffset)) { $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['offset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['size'] = $RIFFchunk[$chunkname][$thisindex]['size']; unset($RIFFchunk[$chunkname][$thisindex]['offset']); @@ -1741,7 +1913,7 @@ class getid3_riff extends getid3_handler { if (isset($RIFFchunk[$chunkname][$thisindex]) && empty($RIFFchunk[$chunkname][$thisindex])) { unset($RIFFchunk[$chunkname][$thisindex]); } - if (isset($RIFFchunk[$chunkname]) && empty($RIFFchunk[$chunkname])) { + if (count($RIFFchunk[$chunkname]) === 0) { unset($RIFFchunk[$chunkname]); } $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['data'] = $this->fread($chunksize); @@ -1765,9 +1937,14 @@ class getid3_riff extends getid3_handler { } } - return $RIFFchunk; + return !empty($RIFFchunk) ? $RIFFchunk : false; } + /** + * @param string $RIFFdata + * + * @return bool + */ public function ParseRIFFdata(&$RIFFdata) { $info = &$this->getid3->info; if ($RIFFdata) { @@ -1805,6 +1982,12 @@ class getid3_riff extends getid3_handler { return false; } + /** + * @param array $RIFFinfoArray + * @param array $CommentsTargetArray + * + * @return bool + */ public static function parseComments(&$RIFFinfoArray, &$CommentsTargetArray) { $RIFFinfoKeyLookup = array( 'IARL'=>'archivallocation', @@ -1864,8 +2047,14 @@ class getid3_riff extends getid3_handler { return true; } + /** + * @param string $WaveFormatExData + * + * @return array + */ public static function parseWAVEFORMATex($WaveFormatExData) { // shortcut + $WaveFormatEx = array(); $WaveFormatEx['raw'] = array(); $WaveFormatEx_raw = &$WaveFormatEx['raw']; @@ -1889,6 +2078,11 @@ class getid3_riff extends getid3_handler { return $WaveFormatEx; } + /** + * @param string $WavPackChunkData + * + * @return bool + */ public function parseWavPackHeader($WavPackChunkData) { // typedef struct { // char ckID [4]; @@ -1950,8 +2144,15 @@ class getid3_riff extends getid3_handler { return true; } + /** + * @param string $BITMAPINFOHEADER + * @param bool $littleEndian + * + * @return array + */ public static function ParseBITMAPINFOHEADER($BITMAPINFOHEADER, $littleEndian=true) { + $parsed = array(); $parsed['biSize'] = substr($BITMAPINFOHEADER, 0, 4); // number of bytes required by the BITMAPINFOHEADER structure $parsed['biWidth'] = substr($BITMAPINFOHEADER, 4, 4); // width of the bitmap in pixels $parsed['biHeight'] = substr($BITMAPINFOHEADER, 8, 4); // height of the bitmap in pixels. If biHeight is positive, the bitmap is a 'bottom-up' DIB and its origin is the lower left corner. If biHeight is negative, the bitmap is a 'top-down' DIB and its origin is the upper left corner @@ -1969,6 +2170,12 @@ class getid3_riff extends getid3_handler { return $parsed; } + /** + * @param string $DIVXTAG + * @param bool $raw + * + * @return array + */ public static function ParseDIVXTAG($DIVXTAG, $raw=false) { // structure from "IDivX" source, Form1.frm, by "Greg Frazier of Daemonic Software Group", email: gfrazier@icestorm.net, web: http://dsg.cjb.net/ // source available at http://files.divx-digest.com/download/c663efe7ef8ad2e90bf4af4d3ea6188a/on0SWN2r/edit/IDivX.zip @@ -2015,6 +2222,7 @@ class getid3_riff extends getid3_handler { 5 => 'NC-17', ); + $parsed = array(); $parsed['title'] = trim(substr($DIVXTAG, 0, 32)); $parsed['artist'] = trim(substr($DIVXTAG, 32, 28)); $parsed['year'] = intval(trim(substr($DIVXTAG, 60, 4))); @@ -2030,8 +2238,8 @@ class getid3_riff extends getid3_handler { if (!$raw) { unset($parsed['genre_id'], $parsed['rating_id']); foreach ($parsed as $key => $value) { - if (!$value === '') { - unset($parsed['key']); + if (empty($value)) { + unset($parsed[$key]); } } } @@ -2043,6 +2251,11 @@ class getid3_riff extends getid3_handler { return $parsed; } + /** + * @param string $tagshortname + * + * @return string + */ public static function waveSNDMtagLookup($tagshortname) { $begin = __LINE__; @@ -2066,6 +2279,11 @@ class getid3_riff extends getid3_handler { return getid3_lib::EmbeddedLookup($tagshortname, $begin, __LINE__, __FILE__, 'riff-sndm'); } + /** + * @param int $wFormatTag + * + * @return string + */ public static function wFormatTagLookup($wFormatTag) { $begin = __LINE__; @@ -2235,6 +2453,11 @@ class getid3_riff extends getid3_handler { return getid3_lib::EmbeddedLookup('0x'.str_pad(strtoupper(dechex($wFormatTag)), 4, '0', STR_PAD_LEFT), $begin, __LINE__, __FILE__, 'riff-wFormatTag'); } + /** + * @param string $fourcc + * + * @return string + */ public static function fourccLookup($fourcc) { $begin = __LINE__; @@ -2629,6 +2852,12 @@ class getid3_riff extends getid3_handler { return getid3_lib::EmbeddedLookup($fourcc, $begin, __LINE__, __FILE__, 'riff-fourcc'); } + /** + * @param string $byteword + * @param bool $signed + * + * @return int|float|false + */ private function EitherEndian2Int($byteword, $signed=false) { if ($this->container == 'riff') { return getid3_lib::LittleEndian2Int($byteword, $signed); @@ -2636,4 +2865,4 @@ class getid3_riff extends getid3_handler { return getid3_lib::BigEndian2Int($byteword, false, $signed); } -} \ No newline at end of file +} diff --git a/lib/getid3/module.audio-video.swf.php b/lib/getid3/module.audio-video.swf.php index 178f439..a9b2c1a 100644 --- a/lib/getid3/module.audio-video.swf.php +++ b/lib/getid3/module.audio-video.swf.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.swf.php // @@ -14,11 +14,22 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_swf extends getid3_handler { + /** + * return all parsed tags if true, otherwise do not return tags not parsed by getID3 + * + * @var bool + */ public $ReturnAllTagData = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -46,7 +57,6 @@ class getid3_swf extends getid3_handler unset($info['swf']); unset($info['fileformat']); return false; - break; } $info['swf']['header']['version'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 3, 1)); $info['swf']['header']['length'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 4, 4)); @@ -110,7 +120,7 @@ class getid3_swf extends getid3_handler $CurrentOffset += 4; } - unset($TagData); + $TagData = array(); $TagData['offset'] = $CurrentOffset; $TagData['size'] = $TagLength; $TagData['id'] = $TagID; diff --git a/lib/getid3/module.audio-video.ts.php b/lib/getid3/module.audio-video.ts.php index 3362024..474503a 100644 --- a/lib/getid3/module.audio-video.ts.php +++ b/lib/getid3/module.audio-video.ts.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio-video.ts.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_ts extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -66,12 +71,16 @@ class getid3_ts extends getid3_handler } } -$this->error('MPEG Transport Stream (.ts) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; + $this->error('MPEG Transport Stream (.ts) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; } - + /** + * @param int $raw + * + * @return string + */ public function TSscramblingControlLookup($raw) { $TSscramblingControlLookup = array(0x00=>'not scrambled', 0x01=>'reserved', 0x02=>'scrambled, even key', 0x03=>'scrambled, odd key'); return (isset($TSscramblingControlLookup[$raw]) ? $TSscramblingControlLookup[$raw] : 'invalid'); diff --git a/lib/getid3/module.audio-video.wtv.php b/lib/getid3/module.audio-video.wtv.php new file mode 100644 index 0000000..ed37abe --- /dev/null +++ b/lib/getid3/module.audio-video.wtv.php @@ -0,0 +1,37 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.audio.wtv.php // +// module for analyzing WTV (Windows Recorded TV Show) // +// audio-video files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_wtv extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $info['fileformat'] = 'wtv'; + $info['video']['dataformat'] = 'wtv'; + + $this->error('WTV (Windows Recorded TV Show) files not properly processed by this version of getID3() ['.$this->getid3->version().']'); + + return true; + } + +} diff --git a/lib/getid3/module.audio.aa.php b/lib/getid3/module.audio.aa.php index 889fd58..171e72d 100644 --- a/lib/getid3/module.audio.aa.php +++ b/lib/getid3/module.audio.aa.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.aa.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_aa extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -36,12 +41,12 @@ class getid3_aa extends getid3_handler $info['fileformat'] = 'aa'; $info['audio']['dataformat'] = 'aa'; -$this->error('Audible Audiobook (.aa) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; + $this->error('Audible Audiobook (.aa) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; $info['audio']['bitrate_mode'] = 'cbr'; // is it? $thisfile_aa['encoding'] = 'ISO-8859-1'; - $thisfile_aa['filesize'] = getid3_lib::BigEndian2Int(substr($AUheader, 0, 4)); + $thisfile_aa['filesize'] = getid3_lib::BigEndian2Int(substr($AAheader, 0, 4)); if ($thisfile_aa['filesize'] > ($info['avdataend'] - $info['avdataoffset'])) { $this->warning('Possible truncated file - expecting "'.$thisfile_aa['filesize'].'" bytes of data, only found '.($info['avdataend'] - $info['avdataoffset']).' bytes"'); } diff --git a/lib/getid3/module.audio.aac.php b/lib/getid3/module.audio.aac.php index 59d79de..5b8448d 100644 --- a/lib/getid3/module.audio.aac.php +++ b/lib/getid3/module.audio.aac.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.aac.php // @@ -14,9 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_aac extends getid3_handler { + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); @@ -28,8 +34,9 @@ class getid3_aac extends getid3_handler return true; } - - + /** + * @return bool + */ public function getAACADIFheaderFilepointer() { $info = &$this->getid3->info; $info['fileformat'] = 'aac'; @@ -69,17 +76,17 @@ class getid3_aac extends getid3_handler $bitoffset += 32; $info['aac']['header']['mpeg_version'] = 4; - $info['aac']['header']['copyright'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); + $info['aac']['header']['copyright'] = substr($AACheaderBitstream, $bitoffset, 1) == '1'; $bitoffset += 1; if ($info['aac']['header']['copyright']) { $info['aac']['header']['copyright_id'] = getid3_lib::Bin2String(substr($AACheaderBitstream, $bitoffset, 72)); $bitoffset += 72; } - $info['aac']['header']['original_copy'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); + $info['aac']['header']['original_copy'] = substr($AACheaderBitstream, $bitoffset, 1) == '1'; $bitoffset += 1; - $info['aac']['header']['home'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); + $info['aac']['header']['home'] = substr($AACheaderBitstream, $bitoffset, 1) == '1'; $bitoffset += 1; - $info['aac']['header']['is_vbr'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); + $info['aac']['header']['is_vbr'] = substr($AACheaderBitstream, $bitoffset, 1) == '1'; $bitoffset += 1; if ($info['aac']['header']['is_vbr']) { $info['audio']['bitrate_mode'] = 'vbr'; @@ -257,7 +264,12 @@ class getid3_aac extends getid3_handler } - + /** + * @param int $MaxFramesToScan + * @param bool $ReturnExtendedInfo + * + * @return bool + */ public function getAACADTSheaderFilepointer($MaxFramesToScan=1000000, $ReturnExtendedInfo=false) { $info = &$this->getid3->info; @@ -388,7 +400,7 @@ class getid3_aac extends getid3_handler if (!isset($BitrateCache[$FrameLength])) { $BitrateCache[$FrameLength] = ($info['aac']['header']['sample_frequency'] / 1024) * $FrameLength * 8; } - getid3_lib::safe_inc($info['aac']['bitrate_distribution'][$BitrateCache[$FrameLength]], 1); + getid3_lib::safe_inc($info['aac']['bitrate_distribution'][(string)$BitrateCache[$FrameLength]], 1); $info['aac'][$framenumber]['aac_frame_length'] = $FrameLength; @@ -443,6 +455,11 @@ class getid3_aac extends getid3_handler // should never get here. } + /** + * @param int $samplerateid + * + * @return int|string + */ public static function AACsampleRateLookup($samplerateid) { static $AACsampleRateLookup = array(); if (empty($AACsampleRateLookup)) { @@ -466,6 +483,12 @@ class getid3_aac extends getid3_handler return (isset($AACsampleRateLookup[$samplerateid]) ? $AACsampleRateLookup[$samplerateid] : 'invalid'); } + /** + * @param int $profileid + * @param int $mpegversion + * + * @return string + */ public static function AACprofileLookup($profileid, $mpegversion) { static $AACprofileLookup = array(); if (empty($AACprofileLookup)) { @@ -481,6 +504,11 @@ class getid3_aac extends getid3_handler return (isset($AACprofileLookup[$mpegversion][$profileid]) ? $AACprofileLookup[$mpegversion][$profileid] : 'invalid'); } + /** + * @param array $program_configs + * + * @return int + */ public static function AACchannelCountCalculate($program_configs) { $channels = 0; for ($i = 0; $i < $program_configs['num_front_channel_elements']; $i++) { diff --git a/lib/getid3/module.audio.ac3.php b/lib/getid3/module.audio.ac3.php index 6834d92..636ff8f 100644 --- a/lib/getid3/module.audio.ac3.php +++ b/lib/getid3/module.audio.ac3.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.ac3.php // @@ -14,14 +14,27 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_ac3 extends getid3_handler { - private $AC3header = array(); - private $BSIoffset = 0; + /** + * @var array + */ + private $AC3header = array(); - const syncword = 0x0B77; + /** + * @var int + */ + private $BSIoffset = 0; + const syncword = 0x0B77; + + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -161,21 +174,21 @@ class getid3_ac3 extends getid3_handler if ($thisfile_ac3_raw_bsi['flags']['timecod1'] & 0x01) { $thisfile_ac3_raw_bsi['timecod1'] = $this->readHeaderBSI(14); // 5.4.2.27 timecod1: Time code first half, 14 bits $thisfile_ac3['timecode1'] = 0; - $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x3E00) >> 9) * 3600; // The first 5 bits of this 14-bit field represent the time in hours, with valid values of 0–23 - $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x01F8) >> 3) * 60; // The next 6 bits represent the time in minutes, with valid values of 0–59 - $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x0003) >> 0) * 8; // The final 3 bits represents the time in 8 second increments, with valid values of 0–7 (representing 0, 8, 16, ... 56 seconds) + $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x3E00) >> 9) * 3600; // The first 5 bits of this 14-bit field represent the time in hours, with valid values of 0�23 + $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x01F8) >> 3) * 60; // The next 6 bits represent the time in minutes, with valid values of 0�59 + $thisfile_ac3['timecode1'] += (($thisfile_ac3_raw_bsi['timecod1'] & 0x0003) >> 0) * 8; // The final 3 bits represents the time in 8 second increments, with valid values of 0�7 (representing 0, 8, 16, ... 56 seconds) } if ($thisfile_ac3_raw_bsi['flags']['timecod1'] & 0x02) { $thisfile_ac3_raw_bsi['timecod2'] = $this->readHeaderBSI(14); // 5.4.2.28 timecod2: Time code second half, 14 bits $thisfile_ac3['timecode2'] = 0; - $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x3800) >> 11) * 1; // The first 3 bits of this 14-bit field represent the time in seconds, with valid values from 0–7 (representing 0-7 seconds) - $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x07C0) >> 6) * (1 / 30); // The next 5 bits represents the time in frames, with valid values from 0–29 (one frame = 1/30th of a second) - $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x003F) >> 0) * ((1 / 30) / 60); // The final 6 bits represents fractions of 1/64 of a frame, with valid values from 0–63 + $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x3800) >> 11) * 1; // The first 3 bits of this 14-bit field represent the time in seconds, with valid values from 0�7 (representing 0-7 seconds) + $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x07C0) >> 6) * (1 / 30); // The next 5 bits represents the time in frames, with valid values from 0�29 (one frame = 1/30th of a second) + $thisfile_ac3['timecode2'] += (($thisfile_ac3_raw_bsi['timecod2'] & 0x003F) >> 0) * ((1 / 30) / 60); // The final 6 bits represents fractions of 1/64 of a frame, with valid values from 0�63 } $thisfile_ac3_raw_bsi['flags']['addbsi'] = (bool) $this->readHeaderBSI(1); if ($thisfile_ac3_raw_bsi['flags']['addbsi']) { - $thisfile_ac3_raw_bsi['addbsi_length'] = $this->readHeaderBSI(6) + 1; // This 6-bit code, which exists only if addbside is a 1, indicates the length in bytes of additional bit stream information. The valid range of addbsil is 0–63, indicating 1–64 additional bytes, respectively. + $thisfile_ac3_raw_bsi['addbsi_length'] = $this->readHeaderBSI(6) + 1; // This 6-bit code, which exists only if addbside is a 1, indicates the length in bytes of additional bit stream information. The valid range of addbsil is 0�63, indicating 1�64 additional bytes, respectively. $this->AC3header['bsi'] .= getid3_lib::BigEndian2Bin($this->fread($thisfile_ac3_raw_bsi['addbsi_length'])); @@ -187,7 +200,7 @@ class getid3_ac3 extends getid3_handler } elseif ($thisfile_ac3_raw_bsi['bsid'] <= 16) { // E-AC3 -$this->error('E-AC3 parsing is incomplete and experimental in this version of getID3 ('.$this->getid3->version().'). Notably the bitrate calculations are wrong -- value might (or not) be correct, but it is not calculated correctly. Email info@getid3.org if you know how to calculate EAC3 bitrate correctly.'); + $this->error('E-AC3 parsing is incomplete and experimental in this version of getID3 ('.$this->getid3->version().'). Notably the bitrate calculations are wrong -- value might (or not) be correct, but it is not calculated correctly. Email info@getid3.org if you know how to calculate EAC3 bitrate correctly.'); $info['audio']['dataformat'] = 'eac3'; $thisfile_ac3_raw_bsi['strmtyp'] = $this->readHeaderBSI(2); @@ -412,7 +425,7 @@ $this->error('E-AC3 parsing is incomplete and experimental in this version of ge } else { $this->error('Bit stream identification is version '.$thisfile_ac3_raw_bsi['bsid'].', but getID3() only understands up to version 16. Please submit a support ticket with a sample file.'); - unset($info['ac3']); + unset($info['ac3']); return false; } @@ -431,11 +444,11 @@ $this->error('E-AC3 parsing is incomplete and experimental in this version of ge $thisfile_ac3['frame_length'] = self::frameSizeLookup($thisfile_ac3_raw_bsi['frmsizecod'], $thisfile_ac3_raw_bsi['fscod']); $thisfile_ac3['bitrate'] = self::bitrateLookup($thisfile_ac3_raw_bsi['frmsizecod']); } elseif (!empty($thisfile_ac3_raw_bsi['frmsiz'])) { -// this isn't right, but it's (usually) close, roughly 5% less than it should be. -// but WHERE is the actual bitrate value stored in EAC3?? email info@getid3.org if you know! + // this isn't right, but it's (usually) close, roughly 5% less than it should be. + // but WHERE is the actual bitrate value stored in EAC3?? email info@getid3.org if you know! $thisfile_ac3['bitrate'] = ($thisfile_ac3_raw_bsi['frmsiz'] + 1) * 16 * 30; // The frmsiz field shall contain a value one less than the overall size of the coded syncframe in 16-bit words. That is, this field may assume a value ranging from 0 to 2047, and these values correspond to syncframe sizes ranging from 1 to 2048. -// kludge-fix to make it approximately the expected value, still not "right": -$thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16000; + // kludge-fix to make it approximately the expected value, still not "right": + $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16000; } $info['audio']['bitrate'] = $thisfile_ac3['bitrate']; @@ -472,6 +485,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return true; } + /** + * @param int $length + * + * @return int + */ private function readHeaderBSI($length) { $data = substr($this->AC3header['bsi'], $this->BSIoffset, $length); $this->BSIoffset += $length; @@ -479,6 +497,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return bindec($data); } + /** + * @param int $fscod + * + * @return int|string|false + */ public static function sampleRateCodeLookup($fscod) { static $sampleRateCodeLookup = array( 0 => 48000, @@ -489,6 +512,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($sampleRateCodeLookup[$fscod]) ? $sampleRateCodeLookup[$fscod] : false); } + /** + * @param int $fscod2 + * + * @return int|string|false + */ public static function sampleRateCodeLookup2($fscod2) { static $sampleRateCodeLookup2 = array( 0 => 24000, @@ -499,6 +527,12 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($sampleRateCodeLookup2[$fscod2]) ? $sampleRateCodeLookup2[$fscod2] : false); } + /** + * @param int $bsmod + * @param int $acmod + * + * @return string|false + */ public static function serviceTypeLookup($bsmod, $acmod) { static $serviceTypeLookup = array(); if (empty($serviceTypeLookup)) { @@ -520,6 +554,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($serviceTypeLookup[$bsmod][$acmod]) ? $serviceTypeLookup[$bsmod][$acmod] : false); } + /** + * @param int $acmod + * + * @return array|false + */ public static function audioCodingModeLookup($acmod) { // array(channel configuration, # channels (not incl LFE), channel order) static $audioCodingModeLookup = array ( @@ -535,6 +574,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($audioCodingModeLookup[$acmod]) ? $audioCodingModeLookup[$acmod] : false); } + /** + * @param int $cmixlev + * + * @return int|float|string|false + */ public static function centerMixLevelLookup($cmixlev) { static $centerMixLevelLookup; if (empty($centerMixLevelLookup)) { @@ -548,6 +592,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($centerMixLevelLookup[$cmixlev]) ? $centerMixLevelLookup[$cmixlev] : false); } + /** + * @param int $surmixlev + * + * @return int|float|string|false + */ public static function surroundMixLevelLookup($surmixlev) { static $surroundMixLevelLookup; if (empty($surroundMixLevelLookup)) { @@ -561,6 +610,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($surroundMixLevelLookup[$surmixlev]) ? $surroundMixLevelLookup[$surmixlev] : false); } + /** + * @param int $dsurmod + * + * @return string|false + */ public static function dolbySurroundModeLookup($dsurmod) { static $dolbySurroundModeLookup = array( 0 => 'not indicated', @@ -571,12 +625,18 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($dolbySurroundModeLookup[$dsurmod]) ? $dolbySurroundModeLookup[$dsurmod] : false); } + /** + * @param int $acmod + * @param bool $lfeon + * + * @return array + */ public static function channelsEnabledLookup($acmod, $lfeon) { $lookup = array( - 'ch1'=>(bool) ($acmod == 0), - 'ch2'=>(bool) ($acmod == 0), - 'left'=>(bool) ($acmod > 1), - 'right'=>(bool) ($acmod > 1), + 'ch1'=>($acmod == 0), + 'ch2'=>($acmod == 0), + 'left'=>($acmod > 1), + 'right'=>($acmod > 1), 'center'=>(bool) ($acmod & 0x01), 'surround_mono'=>false, 'surround_left'=>false, @@ -596,6 +656,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return $lookup; } + /** + * @param int $compre + * + * @return float|int + */ public static function heavyCompression($compre) { // The first four bits indicate gain changes in 6.02dB increments which can be // implemented with an arithmetic shift operation. The following four bits @@ -625,7 +690,7 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 // -8 -42.14 dB $fourbit = str_pad(decbin(($compre & 0xF0) >> 4), 4, '0', STR_PAD_LEFT); - if ($fourbit{0} == '1') { + if ($fourbit[0] == '1') { $log_gain = -8 + bindec(substr($fourbit, 1)); } else { $log_gain = bindec(substr($fourbit, 1)); @@ -646,6 +711,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return $log_gain - $lin_gain; } + /** + * @param int $roomtyp + * + * @return string|false + */ public static function roomTypeLookup($roomtyp) { static $roomTypeLookup = array( 0 => 'not indicated', @@ -656,6 +726,12 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($roomTypeLookup[$roomtyp]) ? $roomTypeLookup[$roomtyp] : false); } + /** + * @param int $frmsizecod + * @param int $fscod + * + * @return int|false + */ public static function frameSizeLookup($frmsizecod, $fscod) { // LSB is whether padding is used or not $padding = (bool) ($frmsizecod & 0x01); @@ -685,13 +761,20 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 18 => array(2560, 2786, 3840) // 640 kbps ); } + $paddingBytes = 0; if (($fscod == 1) && $padding) { // frame lengths are padded by 1 word (16 bits) at 44100 - $frameSizeLookup[$frmsizecod] += 2; + // (fscode==1) means 44100Hz (see sampleRateCodeLookup) + $paddingBytes = 2; } - return (isset($frameSizeLookup[$framesizeid][$fscod]) ? $frameSizeLookup[$framesizeid][$fscod] : false); + return (isset($frameSizeLookup[$framesizeid][$fscod]) ? $frameSizeLookup[$framesizeid][$fscod] + $paddingBytes : false); } + /** + * @param int $frmsizecod + * + * @return int|false + */ public static function bitrateLookup($frmsizecod) { // LSB is whether padding is used or not $padding = (bool) ($frmsizecod & 0x01); @@ -721,6 +804,11 @@ $thisfile_ac3['bitrate'] = round(($thisfile_ac3['bitrate'] * 1.05) / 16000) * 16 return (isset($bitrateLookup[$framesizeid]) ? $bitrateLookup[$framesizeid] : false); } + /** + * @param int $numblkscod + * + * @return int|false + */ public static function blocksPerSyncFrame($numblkscod) { static $blocksPerSyncFrameLookup = array( 0 => 1, diff --git a/lib/getid3/module.audio.amr.php b/lib/getid3/module.audio.amr.php index ff50579..2df9ff6 100644 --- a/lib/getid3/module.audio.amr.php +++ b/lib/getid3/module.audio.amr.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.aa.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_amr extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -57,13 +62,17 @@ class getid3_amr extends getid3_handler } while (strlen($buffer) > 0); $info['playtime_seconds'] = array_sum($thisfile_amr['frame_mode_count']) * 0.020; // each frame contain 160 samples and is 20 milliseconds long - $info['audio']['bitrate'] = (8 * ($info['avdataend'] - $info['avdataoffset'])) / $info['playtime_seconds']; // bitrate could be calculated from average bitrate by distributation of frame types. That would give effective audio bitrate, this gives overall file bitrate which will be a little bit higher since every frame will waste 8 bits for header, plus a few bits for octet padding + $info['audio']['bitrate'] = getid3_lib::SafeDiv(8 * ($info['avdataend'] - $info['avdataoffset']), $info['playtime_seconds']); // bitrate could be calculated from average bitrate by distributation of frame types. That would give effective audio bitrate, this gives overall file bitrate which will be a little bit higher since every frame will waste 8 bits for header, plus a few bits for octet padding $info['bitrate'] = $info['audio']['bitrate']; return true; } - + /** + * @param int $key + * + * @return int|false + */ public function amr_mode_bitrate($key) { static $amr_mode_bitrate = array( 0 => 4750, @@ -78,6 +87,11 @@ class getid3_amr extends getid3_handler return (isset($amr_mode_bitrate[$key]) ? $amr_mode_bitrate[$key] : false); } + /** + * @param int $key + * + * @return int|false + */ public function amr_mode_bytes_per_frame($key) { static $amr_mode_bitrate = array( 0 => 13, // 1-byte frame header + 95 bits [padded to: 12 bytes] audio data diff --git a/lib/getid3/module.audio.au.php b/lib/getid3/module.audio.au.php index 075ee8c..9be3e3f 100644 --- a/lib/getid3/module.audio.au.php +++ b/lib/getid3/module.audio.au.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.au.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_au extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -64,12 +69,17 @@ class getid3_au extends getid3_handler $this->warning('Possible truncated file - expecting "'.$thisfile_au['data_size'].'" bytes of audio data, only found '.($info['avdataend'] - $info['avdataoffset']).' bytes"'); } - $info['playtime_seconds'] = $thisfile_au['data_size'] / ($thisfile_au['sample_rate'] * $thisfile_au['channels'] * ($thisfile_au['used_bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($thisfile_au['data_size'] * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = $thisfile_au['sample_rate'] * $thisfile_au['channels'] * $thisfile_au['used_bits_per_sample']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($thisfile_au['data_size'], $info['audio']['bitrate'] / 8); return true; } + /** + * @param int $id + * + * @return string|false + */ public function AUdataFormatNameLookup($id) { static $AUdataFormatNameLookup = array( 0 => 'unspecified format', @@ -104,6 +114,11 @@ class getid3_au extends getid3_handler return (isset($AUdataFormatNameLookup[$id]) ? $AUdataFormatNameLookup[$id] : false); } + /** + * @param int $id + * + * @return int|false + */ public function AUdataFormatBitsPerSampleLookup($id) { static $AUdataFormatBitsPerSampleLookup = array( 1 => 8, @@ -132,6 +147,11 @@ class getid3_au extends getid3_handler return (isset($AUdataFormatBitsPerSampleLookup[$id]) ? $AUdataFormatBitsPerSampleLookup[$id] : false); } + /** + * @param int $id + * + * @return int|false + */ public function AUdataFormatUsedBitsPerSampleLookup($id) { static $AUdataFormatUsedBitsPerSampleLookup = array( 1 => 8, diff --git a/lib/getid3/module.audio.avr.php b/lib/getid3/module.audio.avr.php index 98666cf..a681889 100644 --- a/lib/getid3/module.audio.avr.php +++ b/lib/getid3/module.audio.avr.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.avr.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_avr extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -115,9 +120,9 @@ class getid3_avr extends getid3_handler $info['audio']['bits_per_sample'] = $info['avr']['bits_per_sample']; $info['audio']['sample_rate'] = $info['avr']['sample_rate']; $info['audio']['channels'] = ($info['avr']['flags']['stereo'] ? 2 : 1); - $info['playtime_seconds'] = ($info['avr']['sample_length'] / $info['audio']['channels']) / $info['avr']['sample_rate']; - $info['audio']['bitrate'] = ($info['avr']['sample_length'] * (($info['avr']['bits_per_sample'] == 8) ? 8 : 16)) / $info['playtime_seconds']; - + $bits_per_sample = ($info['avr']['bits_per_sample'] == 8) ? 8 : 16; + $info['audio']['bitrate'] = $bits_per_sample * $info['audio']['channels'] * $info['avr']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['avr']['sample_length'] * $bits_per_sample, $info['audio']['bitrate']); return true; } diff --git a/lib/getid3/module.audio.bonk.php b/lib/getid3/module.audio.bonk.php index f314a9f..e283631 100644 --- a/lib/getid3/module.audio.bonk.php +++ b/lib/getid3/module.audio.bonk.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.la.php // @@ -14,9 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_bonk extends getid3_handler { + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -41,7 +47,7 @@ class getid3_bonk extends getid3_handler $this->fseek(0 - $BonkTagSize, SEEK_CUR); $BonkTagOffset = $this->ftell(); $TagHeaderTest = $this->fread(5); - if (($TagHeaderTest{0} != "\x00") || (substr($PossibleBonkTag, 4, 4) != strtolower(substr($PossibleBonkTag, 4, 4)))) { + if (($TagHeaderTest[0] != "\x00") || (substr($PossibleBonkTag, 4, 4) != strtolower(substr($PossibleBonkTag, 4, 4)))) { $this->error('Expecting "'.getid3_lib::PrintHexBytes("\x00".strtoupper(substr($PossibleBonkTag, 4, 4))).'" at offset '.$BonkTagOffset.', found "'.getid3_lib::PrintHexBytes($TagHeaderTest).'"'); return false; } @@ -114,6 +120,9 @@ class getid3_bonk extends getid3_handler } + /** + * @param string $BonkTagName + */ public function HandleBonkTags($BonkTagName) { $info = &$this->getid3->info; switch ($BonkTagName) { @@ -145,7 +154,7 @@ class getid3_bonk extends getid3_handler $info['audio']['lossless'] = $thisfile_bonk_BONK['lossless']; $info['audio']['codec'] = 'bonk'; - $info['playtime_seconds'] = $thisfile_bonk_BONK['number_samples'] / ($thisfile_bonk_BONK['sample_rate'] * $thisfile_bonk_BONK['channels']); + $info['playtime_seconds'] = getid3_lib::SafeDiv($thisfile_bonk_BONK['number_samples'], $thisfile_bonk_BONK['sample_rate'] * $thisfile_bonk_BONK['channels']); if ($info['playtime_seconds'] > 0) { $info['audio']['bitrate'] = (($info['bonk']['dataend'] - $info['bonk']['dataoffset']) * 8) / $info['playtime_seconds']; } @@ -195,7 +204,7 @@ class getid3_bonk extends getid3_handler // ID3v2 checking is optional if (class_exists('getid3_id3v2')) { $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_id3v2 = new getid3_id3v2($getid3_temp); $getid3_id3v2->StartingOffset = $info['bonk'][' ID3']['offset'] + 2; $info['bonk'][' ID3']['valid'] = $getid3_id3v2->Analyze(); @@ -213,6 +222,12 @@ class getid3_bonk extends getid3_handler } } + /** + * @param string $PossibleBonkTag + * @param bool $ignorecase + * + * @return bool + */ public static function BonkIsValidTagName($PossibleBonkTag, $ignorecase=false) { static $BonkIsValidTagName = array('BONK', 'INFO', ' ID3', 'META'); foreach ($BonkIsValidTagName as $validtagname) { diff --git a/lib/getid3/module.audio.dsdiff.php b/lib/getid3/module.audio.dsdiff.php new file mode 100644 index 0000000..9afeddf --- /dev/null +++ b/lib/getid3/module.audio.dsdiff.php @@ -0,0 +1,315 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.audio.dsdiff.php // +// module for analyzing Direct Stream Digital Interchange // +// File Format (DSDIFF) files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_dsdiff extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $this->fseek($info['avdataoffset']); + $DSDIFFheader = $this->fread(4); + + // https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf + if (substr($DSDIFFheader, 0, 4) != 'FRM8') { + $this->error('Expecting "FRM8" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSDIFFheader, 0, 4)).'"'); + return false; + } + unset($DSDIFFheader); + $this->fseek($info['avdataoffset']); + + $info['encoding'] = 'ISO-8859-1'; // not certain, but assumed + $info['fileformat'] = 'dsdiff'; + $info['mime_type'] = 'audio/dsd'; + $info['audio']['dataformat'] = 'dsdiff'; + $info['audio']['bitrate_mode'] = 'cbr'; + $info['audio']['bits_per_sample'] = 1; + + $info['dsdiff'] = array(); + $thisChunk = null; + while (!$this->feof() && ($ChunkHeader = $this->fread(12))) { + if (strlen($ChunkHeader) < 12) { + $this->error('Expecting chunk header at offset '.(isset($thisChunk['offset']) ? $thisChunk['offset'] : 'N/A').', found insufficient data in file, aborting parsing'); + break; + } + $thisChunk = array(); + $thisChunk['offset'] = $this->ftell() - 12; + $thisChunk['name'] = substr($ChunkHeader, 0, 4); + if (!preg_match('#^[\\x21-\\x7E]+ *$#', $thisChunk['name'])) { + // "a concatenation of four printable ASCII characters in the range ' ' (space, 0x20) through '~'(0x7E). Space (0x20) cannot precede printing characters; trailing spaces are allowed." + $this->error('Invalid chunk name "'.$thisChunk['name'].'" ('.getid3_lib::PrintHexBytes($thisChunk['name']).') at offset '.$thisChunk['offset'].', aborting parsing'); + } + $thisChunk['size'] = getid3_lib::BigEndian2Int(substr($ChunkHeader, 4, 8)); + $datasize = $thisChunk['size'] + ($thisChunk['size'] % 2); // "If the data is an odd number of bytes in length, a pad byte must be added at the end. The pad byte is not included in ckDataSize." + + switch ($thisChunk['name']) { + case 'FRM8': + $thisChunk['form_type'] = $this->fread(4); + if ($thisChunk['form_type'] != 'DSD ') { + $this->error('Expecting "DSD " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['form_type']).'", aborting parsing'); + break 2; + } + // do nothing further, prevent skipping subchunks + break; + case 'PROP': // PROPerty chunk + $thisChunk['prop_type'] = $this->fread(4); + if ($thisChunk['prop_type'] != 'SND ') { + $this->error('Expecting "SND " at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes($thisChunk['prop_type']).'", aborting parsing'); + break 2; + } + // do nothing further, prevent skipping subchunks + break; + case 'DIIN': // eDIted master INformation chunk + // do nothing, just prevent skipping subchunks + break; + + case 'FVER': // Format VERsion chunk + if ($thisChunk['size'] == 4) { + $FVER = $this->fread(4); + $info['dsdiff']['format_version'] = ord($FVER[0]).'.'.ord($FVER[1]).'.'.ord($FVER[2]).'.'.ord($FVER[3]); + unset($FVER); + } else { + $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk'); + $this->fseek($datasize, SEEK_CUR); + } + break; + case 'FS ': // sample rate chunk + if ($thisChunk['size'] == 4) { + $info['dsdiff']['sample_rate'] = getid3_lib::BigEndian2Int($this->fread(4)); + $info['audio']['sample_rate'] = $info['dsdiff']['sample_rate']; + } else { + $this->warning('Expecting "FVER" chunk to be 4 bytes, found '.$thisChunk['size'].' bytes, skipping chunk'); + $this->fseek($datasize, SEEK_CUR); + } + break; + case 'CHNL': // CHaNneLs chunk + $thisChunk['num_channels'] = getid3_lib::BigEndian2Int($this->fread(2)); + if ($thisChunk['num_channels'] == 0) { + $this->warning('channel count should be greater than zero, skipping chunk'); + $this->fseek($datasize - 2, SEEK_CUR); + } + for ($i = 0; $i < $thisChunk['num_channels']; $i++) { + $thisChunk['channels'][$i] = $this->fread(4); + } + $info['audio']['channels'] = $thisChunk['num_channels']; + break; + case 'CMPR': // CoMPRession type chunk + $thisChunk['compression_type'] = $this->fread(4); + $info['audio']['dataformat'] = trim($thisChunk['compression_type']); + $humanReadableByteLength = getid3_lib::BigEndian2Int($this->fread(1)); + $thisChunk['compression_name'] = $this->fread($humanReadableByteLength); + if (($humanReadableByteLength % 2) == 0) { + // need to seek to multiple of 2 bytes, human-readable string length is only one byte long so if the string is an even number of bytes we need to seek past a padding byte after the string + $this->fseek(1, SEEK_CUR); + } + unset($humanReadableByteLength); + break; + case 'ABSS': // ABSolute Start time chunk + $ABSS = $this->fread(8); + $info['dsdiff']['absolute_start_time']['hours'] = getid3_lib::BigEndian2Int(substr($ABSS, 0, 2)); + $info['dsdiff']['absolute_start_time']['minutes'] = getid3_lib::BigEndian2Int(substr($ABSS, 2, 1)); + $info['dsdiff']['absolute_start_time']['seconds'] = getid3_lib::BigEndian2Int(substr($ABSS, 3, 1)); + $info['dsdiff']['absolute_start_time']['samples'] = getid3_lib::BigEndian2Int(substr($ABSS, 4, 4)); + unset($ABSS); + break; + case 'LSCO': // LoudSpeaker COnfiguration chunk + // 0 = 2-channel stereo set-up + // 3 = 5-channel set-up according to ITU-R BS.775-1 [ITU] + // 4 = 6-channel set-up, 5-channel set-up according to ITU-R BS.775-1 [ITU], plus additional Low Frequency Enhancement (LFE) loudspeaker. Also known as "5.1 configuration" + // 65535 = Undefined channel set-up + $thisChunk['loundspeaker_config_id'] = getid3_lib::BigEndian2Int($this->fread(2)); + break; + case 'COMT': // COMmenTs chunk + $thisChunk['num_comments'] = getid3_lib::BigEndian2Int($this->fread(2)); + for ($i = 0; $i < $thisChunk['num_comments']; $i++) { + $thisComment = array(); + $COMT = $this->fread(14); + $thisComment['creation_year'] = getid3_lib::BigEndian2Int(substr($COMT, 0, 2)); + $thisComment['creation_month'] = getid3_lib::BigEndian2Int(substr($COMT, 2, 1)); + $thisComment['creation_day'] = getid3_lib::BigEndian2Int(substr($COMT, 3, 1)); + $thisComment['creation_hour'] = getid3_lib::BigEndian2Int(substr($COMT, 4, 1)); + $thisComment['creation_minute'] = getid3_lib::BigEndian2Int(substr($COMT, 5, 1)); + $thisComment['comment_type_id'] = getid3_lib::BigEndian2Int(substr($COMT, 6, 2)); + $thisComment['comment_ref_id'] = getid3_lib::BigEndian2Int(substr($COMT, 8, 2)); + $thisComment['string_length'] = getid3_lib::BigEndian2Int(substr($COMT, 10, 4)); + $thisComment['comment_text'] = $this->fread($thisComment['string_length']); + if ($thisComment['string_length'] % 2) { + // commentText[] is the description of the Comment. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count. + $this->fseek(1, SEEK_CUR); + } + $thisComment['comment_type'] = $this->DSDIFFcmtType($thisComment['comment_type_id']); + $thisComment['comment_reference'] = $this->DSDIFFcmtRef($thisComment['comment_type_id'], $thisComment['comment_ref_id']); + $thisComment['creation_unix'] = mktime($thisComment['creation_hour'], $thisComment['creation_minute'], 0, $thisComment['creation_month'], $thisComment['creation_day'], $thisComment['creation_year']); + $thisChunk['comments'][$i] = $thisComment; + + $commentkey = ($thisComment['comment_reference'] ?: 'comment'); + $info['dsdiff']['comments'][$commentkey][] = $thisComment['comment_text']; + unset($thisComment); + } + break; + case 'MARK': // MARKer chunk + $MARK = $this->fread(22); + $thisChunk['marker_hours'] = getid3_lib::BigEndian2Int(substr($MARK, 0, 2)); + $thisChunk['marker_minutes'] = getid3_lib::BigEndian2Int(substr($MARK, 2, 1)); + $thisChunk['marker_seconds'] = getid3_lib::BigEndian2Int(substr($MARK, 3, 1)); + $thisChunk['marker_samples'] = getid3_lib::BigEndian2Int(substr($MARK, 4, 4)); + $thisChunk['marker_offset'] = getid3_lib::BigEndian2Int(substr($MARK, 8, 4)); + $thisChunk['marker_type_id'] = getid3_lib::BigEndian2Int(substr($MARK, 12, 2)); + $thisChunk['marker_channel'] = getid3_lib::BigEndian2Int(substr($MARK, 14, 2)); + $thisChunk['marker_flagraw'] = getid3_lib::BigEndian2Int(substr($MARK, 16, 2)); + $thisChunk['string_length'] = getid3_lib::BigEndian2Int(substr($MARK, 18, 4)); + $thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : ''); + if ($thisChunk['string_length'] % 2) { + // markerText[] is the description of the marker. This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count. + $this->fseek(1, SEEK_CUR); + } + $thisChunk['marker_type'] = $this->DSDIFFmarkType($thisChunk['marker_type_id']); + unset($MARK); + break; + case 'DIAR': // artist chunk + case 'DITI': // title chunk + $thisChunk['string_length'] = getid3_lib::BigEndian2Int($this->fread(4)); + $thisChunk['description'] = ($thisChunk['string_length'] ? $this->fread($thisChunk['string_length']) : ''); + if ($thisChunk['string_length'] % 2) { + // This text must be padded with a byte at the end, if needed, to make it an even number of bytes long. This pad byte, if present, is not included in count. + $this->fseek(1, SEEK_CUR); + } + + $commentKeys = array( + 'DIAR' => 'artist', + 'DITI' => 'title' + ); + $commentkey = $commentKeys[$thisChunk['name']]; + + $info['dsdiff']['comments'][$commentkey][] = $thisChunk['description']; + break; + case 'EMID': // Edited Master ID chunk + if ($thisChunk['size']) { + $thisChunk['identifier'] = $this->fread($thisChunk['size']); + } + break; + + case 'ID3 ': + $endOfID3v2 = $this->ftell() + $datasize; // we will need to reset the filepointer after parsing ID3v2 + + getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); + $getid3_temp = new getID3(); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); + $getid3_id3v2 = new getid3_id3v2($getid3_temp); + $getid3_id3v2->StartingOffset = $this->ftell(); + if ($thisChunk['valid'] = $getid3_id3v2->Analyze()) { + $info['id3v2'] = $getid3_temp->info['id3v2']; + } + unset($getid3_temp, $getid3_id3v2); + + $this->fseek($endOfID3v2); + break; + + case 'DSD ': // DSD sound data chunk + case 'DST ': // DST sound data chunk + // actual audio data, we're not interested, skip + $this->fseek($datasize, SEEK_CUR); + break; + default: + $this->warning('Unhandled chunk "'.$thisChunk['name'].'"'); + $this->fseek($datasize, SEEK_CUR); + break; + } + + @$info['dsdiff']['chunks'][] = $thisChunk; + //break; + } + if (empty($info['audio']['bitrate']) && !empty($info['audio']['channels']) && !empty($info['audio']['sample_rate']) && !empty($info['audio']['bits_per_sample'])) { + $info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels']; + } + + return true; + } + + /** + * @param int $cmtType + * + * @return string + */ + public static function DSDIFFcmtType($cmtType) { + static $DSDIFFcmtType = array( + 0 => 'General (album) Comment', + 1 => 'Channel Comment', + 2 => 'Sound Source', + 3 => 'File History', + ); + return (isset($DSDIFFcmtType[$cmtType]) ? $DSDIFFcmtType[$cmtType] : 'reserved'); + } + + /** + * @param int $cmtType + * @param int $cmtRef + * + * @return string + */ + public static function DSDIFFcmtRef($cmtType, $cmtRef) { + static $DSDIFFcmtRef = array( + 2 => array( // Sound Source + 0 => 'DSD recording', + 1 => 'Analogue recording', + 2 => 'PCM recording', + ), + 3 => array( // File History + 0 => 'comment', // General Remark + 1 => 'encodeby', // Name of the operator + 2 => 'encoder', // Name or type of the creating machine + 3 => 'timezone', // Time zone information + 4 => 'revision', // Revision of the file + ), + ); + switch ($cmtType) { + case 0: + // If the comment type is General Comment the comment reference must be 0 + return ''; + case 1: + // If the comment type is Channel Comment, the comment reference defines the channel number to which the comment belongs + return ($cmtRef ? 'channel '.$cmtRef : 'all channels'); + case 2: + case 3: + return (isset($DSDIFFcmtRef[$cmtType][$cmtRef]) ? $DSDIFFcmtRef[$cmtType][$cmtRef] : 'reserved'); + } + return 'unsupported $cmtType='.$cmtType; + } + + /** + * @param int $markType + * + * @return string + */ + public static function DSDIFFmarkType($markType) { + static $DSDIFFmarkType = array( + 0 => 'TrackStart', // Entry point for a Track start + 1 => 'TrackStop', // Entry point for ending a Track + 2 => 'ProgramStart', // Start point of 2-channel or multi-channel area + 3 => 'Obsolete', // + 4 => 'Index', // Entry point of an Index + ); + return (isset($DSDIFFmarkType[$markType]) ? $DSDIFFmarkType[$markType] : 'reserved'); + } + +} diff --git a/lib/getid3/module.audio.dsf.php b/lib/getid3/module.audio.dsf.php index 50be37c..d8e4342 100644 --- a/lib/getid3/module.audio.dsf.php +++ b/lib/getid3/module.audio.dsf.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.dsf.php // @@ -14,11 +14,16 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); class getid3_dsf extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -110,12 +115,16 @@ class getid3_dsf extends getid3_handler $info['audio']['sample_rate'] = $info['dsf']['fmt']['sample_rate']; $info['audio']['channels'] = $info['dsf']['fmt']['channels']; $info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels']; - $info['playtime_seconds'] = ($info['dsf']['data']['data_chunk_size'] * 8) / $info['audio']['bitrate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['dsf']['data']['data_chunk_size'] * 8, $info['audio']['bitrate']); return true; } - + /** + * @param int $channel_type_id + * + * @return string + */ public static function DSFchannelTypeLookup($channel_type_id) { static $DSFchannelTypeLookup = array( // interleaving order: diff --git a/lib/getid3/module.audio.dss.php b/lib/getid3/module.audio.dss.php index 6bd9668..dd2a56b 100644 --- a/lib/getid3/module.audio.dss.php +++ b/lib/getid3/module.audio.dss.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.dss.php // @@ -14,18 +14,23 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_dss extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); $DSSheader = $this->fread(1540); - if (!preg_match('#^[\\x02-\\x06]ds[s2]#', $DSSheader)) { - $this->error('Expecting "[02-06] 64 73 [73|32]" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSSheader, 0, 4)).'"'); + if (!preg_match('#^[\\x02-\\x08]ds[s2]#', $DSSheader)) { + $this->error('Expecting "[02-08] 64 73 [73|32]" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSSheader, 0, 4)).'"'); return false; } @@ -44,7 +49,7 @@ class getid3_dss extends getid3_handler // 32-37 = "FE FF FE FF F7 FF" in all the sample files I've seen $info['dss']['date_create_unix'] = $this->DSSdateStringToUnixDate(substr($DSSheader, 38, 12)); $info['dss']['date_complete_unix'] = $this->DSSdateStringToUnixDate(substr($DSSheader, 50, 12)); - $info['dss']['playtime_sec'] = intval((substr($DSSheader, 62, 2) * 3600) + (substr($DSSheader, 64, 2) * 60) + substr($DSSheader, 66, 2)); // approximate file playtime in HHMMSS + $info['dss']['playtime_sec'] = ((int) substr($DSSheader, 62, 2) * 3600) + ((int) substr($DSSheader, 64, 2) * 60) + (int) substr($DSSheader, 66, 2); // approximate file playtime in HHMMSS if ($info['dss']['version'] <= 3) { $info['dss']['playtime_ms'] = getid3_lib::LittleEndian2Int(substr($DSSheader, 512, 4)); // exact file playtime in milliseconds. Has also been observed at offset 530 in one sample file, with something else (unknown) at offset 512 $info['dss']['priority'] = ord(substr($DSSheader, 793, 1)); @@ -71,8 +76,13 @@ class getid3_dss extends getid3_handler return true; } + /** + * @param string $datestring + * + * @return int|false + */ public function DSSdateStringToUnixDate($datestring) { - $y = substr($datestring, 0, 2); + $y = (int) substr($datestring, 0, 2); $m = substr($datestring, 2, 2); $d = substr($datestring, 4, 2); $h = substr($datestring, 6, 2); @@ -82,6 +92,11 @@ class getid3_dss extends getid3_handler return mktime($h, $i, $s, $m, $d, $y); } + /** + * @param int $sample_rate_index + * + * @return int|false + */ public function DSSsampleRateLookup($sample_rate_index) { static $dssSampleRateLookup = array( 0x0A => 16000, diff --git a/lib/getid3/module.audio.dts.php b/lib/getid3/module.audio.dts.php index bdc78f0..ff1a88f 100644 --- a/lib/getid3/module.audio.dts.php +++ b/lib/getid3/module.audio.dts.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.dts.php // @@ -14,6 +14,9 @@ // // ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} /** * @tutorial http://wiki.multimedia.cx/index.php?title=DTS @@ -21,21 +24,27 @@ class getid3_dts extends getid3_handler { /** - * Default DTS syncword used in native .cpt or .dts formats - */ - const syncword = "\x7F\xFE\x80\x01"; + * Default DTS syncword used in native .cpt or .dts formats. + */ + const syncword = "\x7F\xFE\x80\x01"; + /** + * @var int + */ private $readBinDataOffset = 0; - /** - * Possible syncwords indicating bitstream encoding - */ - public static $syncwords = array( - 0 => "\x7F\xFE\x80\x01", // raw big-endian - 1 => "\xFE\x7F\x01\x80", // raw little-endian - 2 => "\x1F\xFF\xE8\x00", // 14-bit big-endian - 3 => "\xFF\x1F\x00\xE8"); // 14-bit little-endian + /** + * Possible syncwords indicating bitstream encoding. + */ + public static $syncwords = array( + 0 => "\x7F\xFE\x80\x01", // raw big-endian + 1 => "\xFE\x7F\x01\x80", // raw little-endian + 2 => "\x1F\xFF\xE8\x00", // 14-bit big-endian + 3 => "\xFF\x1F\x00\xE8"); // 14-bit little-endian + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $info['fileformat'] = 'dts'; @@ -45,18 +54,18 @@ class getid3_dts extends getid3_handler // check syncword $sync = substr($DTSheader, 0, 4); - if (($encoding = array_search($sync, self::$syncwords)) !== false) { + if (($encoding = array_search($sync, self::$syncwords)) !== false) { - $info['dts']['raw']['magic'] = $sync; + $info['dts']['raw']['magic'] = $sync; $this->readBinDataOffset = 32; - } elseif ($this->isDependencyFor('matroska')) { + } elseif ($this->isDependencyFor('matroska')) { // Matroska contains DTS without syncword encoded as raw big-endian format $encoding = 0; $this->readBinDataOffset = 0; - } else { + } else { unset($info['fileformat']); return $this->error('Expecting "'.implode('| ', array_map('getid3_lib::PrintHexBytes', self::$syncwords)).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($sync).'"'); @@ -139,6 +148,12 @@ class getid3_dts extends getid3_handler return true; } + /** + * @param string $bin + * @param int $length + * + * @return int + */ private function readBinData($bin, $length) { $data = substr($bin, $this->readBinDataOffset, $length); $this->readBinDataOffset += $length; @@ -146,6 +161,11 @@ class getid3_dts extends getid3_handler return bindec($data); } + /** + * @param int $index + * + * @return int|string|false + */ public static function bitrateLookup($index) { static $lookup = array( 0 => 32000, @@ -184,6 +204,11 @@ class getid3_dts extends getid3_handler return (isset($lookup[$index]) ? $lookup[$index] : false); } + /** + * @param int $index + * + * @return int|string|false + */ public static function sampleRateLookup($index) { static $lookup = array( 0 => 'invalid', @@ -206,6 +231,11 @@ class getid3_dts extends getid3_handler return (isset($lookup[$index]) ? $lookup[$index] : false); } + /** + * @param int $index + * + * @return int|false + */ public static function bitPerSampleLookup($index) { static $lookup = array( 0 => 16, @@ -216,44 +246,46 @@ class getid3_dts extends getid3_handler return (isset($lookup[$index]) ? $lookup[$index] : false); } + /** + * @param int $index + * + * @return int|false + */ public static function numChannelsLookup($index) { switch ($index) { case 0: return 1; - break; case 1: case 2: case 3: case 4: return 2; - break; case 5: case 6: return 3; - break; case 7: case 8: return 4; - break; case 9: return 5; - break; case 10: case 11: case 12: return 6; - break; case 13: return 7; - break; case 14: case 15: return 8; - break; } return false; } + /** + * @param int $index + * + * @return string + */ public static function channelArrangementLookup($index) { static $lookup = array( 0 => 'A', @@ -276,14 +308,18 @@ class getid3_dts extends getid3_handler return (isset($lookup[$index]) ? $lookup[$index] : 'user-defined'); } + /** + * @param int $index + * @param int $version + * + * @return int|false + */ public static function dialogNormalization($index, $version) { switch ($version) { case 7: return 0 - $index; - break; case 6: return 0 - 16 - $index; - break; } return false; } diff --git a/lib/getid3/module.audio.flac.php b/lib/getid3/module.audio.flac.php index 348cce3..014061d 100644 --- a/lib/getid3/module.audio.flac.php +++ b/lib/getid3/module.audio.flac.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.flac.php // @@ -14,7 +14,9 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, true); /** @@ -24,6 +26,9 @@ class getid3_flac extends getid3_handler { const syncword = 'fLaC'; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -41,22 +46,30 @@ class getid3_flac extends getid3_handler return $this->parseMETAdata(); } + /** + * @return bool + */ public function parseMETAdata() { $info = &$this->getid3->info; do { $BlockOffset = $this->ftell(); $BlockHeader = $this->fread(4); - $LBFBT = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1)); + $LBFBT = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1)); // LBFBT = LastBlockFlag + BlockType $LastBlockFlag = (bool) ($LBFBT & 0x80); $BlockType = ($LBFBT & 0x7F); $BlockLength = getid3_lib::BigEndian2Int(substr($BlockHeader, 1, 3)); $BlockTypeText = self::metaBlockTypeLookup($BlockType); if (($BlockOffset + 4 + $BlockLength) > $info['avdataend']) { - $this->error('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file'); + $this->warning('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file'); break; } if ($BlockLength < 1) { + if ($BlockTypeText != 'reserved') { + // probably supposed to be zero-length + $this->warning('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockTypeText.') at offset '.$BlockOffset.' is zero bytes'); + continue; + } $this->error('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockLength.') at offset '.$BlockOffset.' is invalid'); break; } @@ -167,7 +180,7 @@ class getid3_flac extends getid3_handler if (isset($info['flac']['STREAMINFO']['audio_signature'])) { if ($info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) { - $this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)'); + $this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)'); } else { $info['md5_data_source'] = ''; @@ -194,12 +207,14 @@ class getid3_flac extends getid3_handler return true; } - private function parseSTREAMINFO($BlockData) { - $info = &$this->getid3->info; - - $info['flac']['STREAMINFO'] = array(); - $streaminfo = &$info['flac']['STREAMINFO']; + /** + * @param string $BlockData + * + * @return array + */ + public static function parseSTREAMINFOdata($BlockData) { + $streaminfo = array(); $streaminfo['min_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 0, 2)); $streaminfo['max_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 2, 2)); $streaminfo['min_frame_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 4, 3)); @@ -211,15 +226,28 @@ class getid3_flac extends getid3_handler $streaminfo['bits_per_sample'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 23, 5)) + 1; $streaminfo['samples_stream'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 28, 36)); - $streaminfo['audio_signature'] = substr($BlockData, 18, 16); + $streaminfo['audio_signature'] = substr($BlockData, 18, 16); - if (!empty($streaminfo['sample_rate'])) { + return $streaminfo; + } + + /** + * @param string $BlockData + * + * @return bool + */ + private function parseSTREAMINFO($BlockData) { + $info = &$this->getid3->info; + + $info['flac']['STREAMINFO'] = self::parseSTREAMINFOdata($BlockData); + + if (!empty($info['flac']['STREAMINFO']['sample_rate'])) { $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['sample_rate'] = $streaminfo['sample_rate']; - $info['audio']['channels'] = $streaminfo['channels']; - $info['audio']['bits_per_sample'] = $streaminfo['bits_per_sample']; - $info['playtime_seconds'] = $streaminfo['samples_stream'] / $streaminfo['sample_rate']; + $info['audio']['sample_rate'] = $info['flac']['STREAMINFO']['sample_rate']; + $info['audio']['channels'] = $info['flac']['STREAMINFO']['channels']; + $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample']; + $info['playtime_seconds'] = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate']; if ($info['playtime_seconds'] > 0) { if (!$this->isDependencyFor('matroska')) { $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; @@ -236,6 +264,11 @@ class getid3_flac extends getid3_handler return true; } + /** + * @param string $BlockData + * + * @return bool + */ private function parseAPPLICATION($BlockData) { $info = &$this->getid3->info; @@ -246,6 +279,11 @@ class getid3_flac extends getid3_handler return true; } + /** + * @param string $BlockData + * + * @return bool + */ private function parseSEEKTABLE($BlockData) { $info = &$this->getid3->info; @@ -275,6 +313,11 @@ class getid3_flac extends getid3_handler return true; } + /** + * @param string $BlockData + * + * @return bool + */ private function parseVORBIS_COMMENT($BlockData) { $info = &$this->getid3->info; @@ -294,6 +337,11 @@ class getid3_flac extends getid3_handler return true; } + /** + * @param string $BlockData + * + * @return bool + */ private function parseCUESHEET($BlockData) { $info = &$this->getid3->info; $offset = 0; @@ -346,12 +394,15 @@ class getid3_flac extends getid3_handler } /** - * Parse METADATA_BLOCK_PICTURE flac structure and extract attachment - * External usage: audio.ogg - */ + * Parse METADATA_BLOCK_PICTURE flac structure and extract attachment + * External usage: audio.ogg + * + * @return bool + */ public function parsePICTURE() { $info = &$this->getid3->info; + $picture = array(); $picture['typeid'] = getid3_lib::BigEndian2Int($this->fread(4)); $picture['picturetype'] = self::pictureTypeLookup($picture['typeid']); $picture['image_mime'] = $this->fread(getid3_lib::BigEndian2Int($this->fread(4))); @@ -380,6 +431,11 @@ class getid3_flac extends getid3_handler return true; } + /** + * @param int $blocktype + * + * @return string + */ public static function metaBlockTypeLookup($blocktype) { static $lookup = array( 0 => 'STREAMINFO', @@ -393,6 +449,11 @@ class getid3_flac extends getid3_handler return (isset($lookup[$blocktype]) ? $lookup[$blocktype] : 'reserved'); } + /** + * @param int $applicationid + * + * @return string + */ public static function applicationIDLookup($applicationid) { // http://flac.sourceforge.net/id.html static $lookup = array( @@ -423,6 +484,11 @@ class getid3_flac extends getid3_handler return (isset($lookup[$applicationid]) ? $lookup[$applicationid] : 'reserved'); } + /** + * @param int $type_id + * + * @return string + */ public static function pictureTypeLookup($type_id) { static $lookup = array ( 0 => 'Other', diff --git a/lib/getid3/module.audio.la.php b/lib/getid3/module.audio.la.php index f46c9aa..27a4679 100644 --- a/lib/getid3/module.audio.la.php +++ b/lib/getid3/module.audio.la.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.la.php // @@ -14,11 +14,16 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); class getid3_la extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -213,7 +218,6 @@ class getid3_la extends getid3_handler $this->error('Not a LA (Lossless-Audio) file'); } return false; - break; } $info['audio']['channels'] = $info['la']['channels']; diff --git a/lib/getid3/module.audio.lpac.php b/lib/getid3/module.audio.lpac.php index 447cc2d..427cf66 100644 --- a/lib/getid3/module.audio.lpac.php +++ b/lib/getid3/module.audio.lpac.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.lpac.php // @@ -14,20 +14,27 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); class getid3_lpac extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); $LPACheader = $this->fread(14); - if (substr($LPACheader, 0, 4) != 'LPAC') { + $StreamMarker = substr($LPACheader, 0, 4); + if ($StreamMarker != 'LPAC') { $this->error('Expected "LPAC" at offset '.$info['avdataoffset'].', found "'.$StreamMarker.'"'); return false; } + $flags = array(); $info['avdataoffset'] += 14; $info['fileformat'] = 'lpac'; @@ -77,7 +84,7 @@ class getid3_lpac extends getid3_handler } $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info = $info; $getid3_riff = new getid3_riff($getid3_temp); $getid3_riff->Analyze(); @@ -119,8 +126,8 @@ class getid3_lpac extends getid3_handler } } - $info['playtime_seconds'] = $info['lpac']['total_samples'] / $info['audio']['sample_rate']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['lpac']['total_samples'], $info['audio']['sample_rate']); + $info['audio']['bitrate'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); return true; } diff --git a/lib/getid3/module.audio.midi.php b/lib/getid3/module.audio.midi.php index 359aca2..d79a7f0 100644 --- a/lib/getid3/module.audio.midi.php +++ b/lib/getid3/module.audio.midi.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.midi.php // @@ -14,13 +14,25 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + define('GETID3_MIDI_MAGIC_MTHD', 'MThd'); // MIDI file header magic define('GETID3_MIDI_MAGIC_MTRK', 'MTrk'); // MIDI track header magic class getid3_midi extends getid3_handler { + /** + * if false only parse most basic information, much faster for some files but may be inaccurate + * + * @var bool + */ public $scanwholefile = true; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -51,6 +63,7 @@ class getid3_midi extends getid3_handler $thisfile_midi_raw['ticksperqnote'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2)); $offset += 2; + $trackdataarray = array(); for ($i = 0; $i < $thisfile_midi_raw['tracks']; $i++) { while ((strlen($MIDIdata) - $offset) < 8) { if ($buffer = $this->fread($this->getid3->fread_buffer_size())) { @@ -75,7 +88,7 @@ class getid3_midi extends getid3_handler } } - if (!isset($trackdataarray) || !is_array($trackdataarray)) { + if (!is_array($trackdataarray) || count($trackdataarray) === 0) { $this->error('Cannot find MIDI track information'); unset($thisfile_midi); unset($info['fileformat']); @@ -86,8 +99,8 @@ class getid3_midi extends getid3_handler $thisfile_midi['totalticks'] = 0; $info['playtime_seconds'] = 0; $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat - $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat $MicroSecondsPerQuarterNoteAfter = array (); + $MIDIevents = array(); foreach ($trackdataarray as $tracknumber => $trackdata) { @@ -230,15 +243,14 @@ class getid3_midi extends getid3_handler return false; } $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat; - $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60; $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat; $TicksAtCurrentBPM = 0; break; case 0x58: // Time signature - $timesig_numerator = getid3_lib::BigEndian2Int($METAeventData{0}); - $timesig_denominator = pow(2, getid3_lib::BigEndian2Int($METAeventData{1})); // $02 -> x/4, $03 -> x/8, etc - $timesig_32inqnote = getid3_lib::BigEndian2Int($METAeventData{2}); // number of 32nd notes to the quarter note + $timesig_numerator = getid3_lib::BigEndian2Int($METAeventData[0]); + $timesig_denominator = pow(2, getid3_lib::BigEndian2Int($METAeventData[1])); // $02 -> x/4, $03 -> x/8, etc + $timesig_32inqnote = getid3_lib::BigEndian2Int($METAeventData[2]); // number of 32nd notes to the quarter note //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_32inqnote'] = $timesig_32inqnote; //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_numerator'] = $timesig_numerator; //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_denominator'] = $timesig_denominator; @@ -247,13 +259,13 @@ class getid3_midi extends getid3_handler break; case 0x59: // Keysignature - $keysig_sharpsflats = getid3_lib::BigEndian2Int($METAeventData{0}); + $keysig_sharpsflats = getid3_lib::BigEndian2Int($METAeventData[0]); if ($keysig_sharpsflats & 0x80) { // (-7 -> 7 flats, 0 ->key of C, 7 -> 7 sharps) $keysig_sharpsflats -= 256; } - $keysig_majorminor = getid3_lib::BigEndian2Int($METAeventData{1}); // 0 -> major, 1 -> minor + $keysig_majorminor = getid3_lib::BigEndian2Int($METAeventData[1]); // 0 -> major, 1 -> minor $keysigs = array(-7=>'Cb', -6=>'Gb', -5=>'Db', -4=>'Ab', -3=>'Eb', -2=>'Bb', -1=>'F', 0=>'C', 1=>'G', 2=>'D', 3=>'A', 4=>'E', 5=>'B', 6=>'F#', 7=>'C#'); //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_sharps'] = (($keysig_sharpsflats > 0) ? abs($keysig_sharpsflats) : 0); //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_flats'] = (($keysig_sharpsflats < 0) ? abs($keysig_sharpsflats) : 0); @@ -283,7 +295,8 @@ class getid3_midi extends getid3_handler $thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime); } } - $previoustickoffset = null; + $previoustickoffset = null; + $prevmicrosecondsperbeat = null; ksort($MicroSecondsPerQuarterNoteAfter); foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) { @@ -312,7 +325,7 @@ class getid3_midi extends getid3_handler return false; } - $info['playtime_seconds'] += (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($microsecondsperbeat / 1000000); + $info['playtime_seconds'] += (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000); } } @@ -329,6 +342,11 @@ class getid3_midi extends getid3_handler return true; } + /** + * @param int $instrumentid + * + * @return string + */ public function GeneralMIDIinstrumentLookup($instrumentid) { $begin = __LINE__; @@ -469,6 +487,11 @@ class getid3_midi extends getid3_handler return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIinstrument'); } + /** + * @param int $instrumentid + * + * @return string + */ public function GeneralMIDIpercussionLookup($instrumentid) { $begin = __LINE__; diff --git a/lib/getid3/module.audio.mod.php b/lib/getid3/module.audio.mod.php index 4b888ec..4841c64 100644 --- a/lib/getid3/module.audio.mod.php +++ b/lib/getid3/module.audio.mod.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.mod.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_mod extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); @@ -26,31 +31,75 @@ class getid3_mod extends getid3_handler return $this->getITheaderFilepointer(); } elseif (preg_match('#^Extended Module#', $fileheader)) { return $this->getXMheaderFilepointer(); - } elseif (preg_match('#^.{44}SCRM#', $fileheader)) { + } elseif (preg_match('#^.{44}SCRM#s', $fileheader)) { return $this->getS3MheaderFilepointer(); - } elseif (preg_match('#^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)#', $fileheader)) { + //} elseif (preg_match('#^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)#s', $fileheader)) { + } elseif (preg_match('#^.{1080}(M\\.K\\.)#s', $fileheader)) { + /* + The four letters "M.K." - This is something Mahoney & Kaktus inserted when they + increased the number of samples from 15 to 31. If it's not there, the module/song + uses 15 samples or the text has been removed to make the module harder to rip. + Startrekker puts "FLT4" or "FLT8" there instead. + If there are more than 64 patterns, PT2.3 will insert M!K! here. + */ return $this->getMODheaderFilepointer(); } $this->error('This is not a known type of MOD file'); return false; } - + /** + * @return bool + */ public function getMODheaderFilepointer() { $info = &$this->getid3->info; - $this->fseek($info['avdataoffset'] + 1080); - $FormatID = $this->fread(4); - if (!preg_match('#^(M.K.|[5-9]CHN|[1-3][0-9]CH)$#', $FormatID)) { - $this->error('This is not a known type of MOD file'); + $this->fseek($info['avdataoffset']); + $filedata = $this->fread(1084); + //if (!preg_match('#^(M.K.|[5-9]CHN|[1-3][0-9]CH)$#', $FormatID)) { + if (substr($filedata, 1080, 4) == 'M.K.') { + + // + 0 song/module working title + // + 20 15 sample headers (see below) + // + 470 song length (number of steps in pattern table) + // + 471 song speed in beats per minute (see below) + // + 472 pattern step table + $offset = 0; + $info['mod']['title'] = rtrim(substr($filedata, $offset, 20), "\x00"); $offset += 20; + + $info['tags']['mod']['title'] = array($info['mod']['title']); + + for ($samplenumber = 0; $samplenumber <= 30; $samplenumber++) { + $sampledata = array(); + $sampledata['name'] = substr($filedata, $offset, 22); $offset += 22; + $sampledata['length'] = getid3_lib::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['volume'] = getid3_lib::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['repeat_offset'] = getid3_lib::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['repeat_length'] = getid3_lib::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $info['mod']['samples'][$samplenumber] = $sampledata; + } + + $info['mod']['song_length'] = getid3_lib::BigEndian2Int(substr($filedata, $offset++, 1));// Songlength. Range is 1-128. + $info['mod']['bpm'] = getid3_lib::BigEndian2Int(substr($filedata, $offset++, 1));// This byte is set to 127, so that old trackers will search through all patterns when loading. Noisetracker uses this byte for restart, ProTracker doesn't. + + for ($songposition = 0; $songposition <= 127; $songposition++) { + // Song positions 0-127. Each hold a number from 0-63 (or 0-127) + // that tells the tracker what pattern to play at that position. + $info['mod']['song_positions'][$songposition] = getid3_lib::BigEndian2Int(substr($filedata, $offset++, 1)); + } + + } else { + $this->error('unknown MOD ID at offset 1080: '.getid3_lib::PrintHexBytes(substr($filedata, 1080, 4))); return false; } - $info['fileformat'] = 'mod'; - $this->error('MOD parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); - return false; +$this->warning('MOD (SoundTracker) parsing incomplete in this version of getID3() ['.$this->getid3->version().']'); + return true; } + /** + * @return bool + */ public function getXMheaderFilepointer() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); @@ -66,6 +115,9 @@ class getid3_mod extends getid3_handler return false; } + /** + * @return bool + */ public function getS3MheaderFilepointer() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset'] + 44); @@ -81,6 +133,9 @@ class getid3_mod extends getid3_handler return false; } + /** + * @return bool + */ public function getITheaderFilepointer() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); diff --git a/lib/getid3/module.audio.monkey.php b/lib/getid3/module.audio.monkey.php index afa2eaf..beaca8c 100644 --- a/lib/getid3/module.audio.monkey.php +++ b/lib/getid3/module.audio.monkey.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.monkey.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_monkey extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -160,7 +165,7 @@ class getid3_monkey extends getid3_handler $info['md5_data_source'] = ''; $md5 = $thisfile_monkeysaudio_raw['cFileMD5']; for ($i = 0; $i < strlen($md5); $i++) { - $info['md5_data_source'] .= str_pad(dechex(ord($md5{$i})), 2, '00', STR_PAD_LEFT); + $info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT); } if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) { unset($info['md5_data_source']); @@ -177,6 +182,11 @@ class getid3_monkey extends getid3_handler return true; } + /** + * @param int $compressionlevel + * + * @return string + */ public function MonkeyCompressionLevelNameLookup($compressionlevel) { static $MonkeyCompressionLevelNameLookup = array( 0 => 'unknown', @@ -189,6 +199,12 @@ class getid3_monkey extends getid3_handler return (isset($MonkeyCompressionLevelNameLookup[$compressionlevel]) ? $MonkeyCompressionLevelNameLookup[$compressionlevel] : 'invalid'); } + /** + * @param int $versionid + * @param int $compressionlevel + * + * @return int + */ public function MonkeySamplesPerFrame($versionid, $compressionlevel) { if ($versionid >= 3950) { return 73728 * 4; diff --git a/lib/getid3/module.audio.mp3.php b/lib/getid3/module.audio.mp3.php index ed2d844..0d8fee3 100644 --- a/lib/getid3/module.audio.mp3.php +++ b/lib/getid3/module.audio.mp3.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.mp3.php // @@ -14,19 +14,33 @@ // /// ///////////////////////////////////////////////////////////////// - -// number of frames to scan to determine if MPEG-audio sequence is valid -// Lower this number to 5-20 for faster scanning -// Increase this number to 50+ for most accurate detection of valid VBR/CBR -// mpeg-audio streams -define('GETID3_MP3_VALID_CHECK_FRAMES', 35); +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_mp3 extends getid3_handler { + /** + * Forces getID3() to scan the file byte-by-byte and log all the valid audio frame headers - extremely slow, + * unrecommended, but may provide data from otherwise-unusable files. + * + * @var bool + */ + public $allow_bruteforce = false; - public $allow_bruteforce = false; // forces getID3() to scan the file byte-by-byte and log all the valid audio frame headers - extremely slow, unrecommended, but may provide data from otherwise-unusuable files + /** + * number of frames to scan to determine if MPEG-audio sequence is valid + * Lower this number to 5-20 for faster scanning + * Increase this number to 50+ for most accurate detection of valid VBR/CBR mpeg-audio streams + * + * @var int + */ + public $mp3_valid_check_frames = 50; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -35,7 +49,7 @@ class getid3_mp3 extends getid3_handler if (!$this->getOnlyMPEGaudioInfo($info['avdataoffset'])) { if ($this->allow_bruteforce) { $this->error('Rescanning file in BruteForce mode'); - $this->getOnlyMPEGaudioInfoBruteForce($this->getid3->fp, $info); + $this->getOnlyMPEGaudioInfoBruteForce(); } } @@ -44,6 +58,7 @@ class getid3_mp3 extends getid3_handler $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); } + $CurrentDataLAMEversionString = null; if (((isset($info['id3v2']['headerlength']) && ($info['avdataoffset'] > $info['id3v2']['headerlength'])) || (!isset($info['id3v2']) && ($info['avdataoffset'] > 0) && ($info['avdataoffset'] != $initialOffset)))) { $synchoffsetwarning = 'Unknown data before synch '; @@ -110,6 +125,12 @@ class getid3_mp3 extends getid3_handler if (substr($PossiblyLongerLAMEversion_String, 0, strlen($CurrentDataLAMEversionString)) == $CurrentDataLAMEversionString) { $PossiblyLongerLAMEversion_NewString = substr($PossiblyLongerLAMEversion_String, 0, strspn($PossiblyLongerLAMEversion_String, 'LAME0123456789., (abcdefghijklmnopqrstuvwxyzJFSOND)')); //"LAME3.90.3" "LAME3.87 (beta 1, Sep 27 2000)" "LAME3.88 (beta)" if (empty($info['audio']['encoder']) || (strlen($PossiblyLongerLAMEversion_NewString) > strlen($info['audio']['encoder']))) { + if (!empty($info['audio']['encoder']) && !empty($info['mpeg']['audio']['LAME']['short_version']) && ($info['audio']['encoder'] == $info['mpeg']['audio']['LAME']['short_version'])) { + if (preg_match('#^LAME[0-9\\.]+#', $PossiblyLongerLAMEversion_NewString, $matches)) { + // "LAME3.100" -> "LAME3.100.1", but avoid including "(alpha)" and similar + $info['mpeg']['audio']['LAME']['short_version'] = $matches[0]; + } + } $info['audio']['encoder'] = $PossiblyLongerLAMEversion_NewString; } } @@ -152,7 +173,11 @@ class getid3_mp3 extends getid3_handler // Calculate playtime if (!isset($info['playtime_seconds']) && isset($info['audio']['bitrate']) && ($info['audio']['bitrate'] > 0)) { - $info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['audio']['bitrate']; + // https://github.com/JamesHeinrich/getID3/issues/161 + // VBR header frame contains ~0.026s of silent audio data, but is not actually part of the original encoding and should be ignored + $xingVBRheaderFrameLength = ((isset($info['mpeg']['audio']['VBR_frames']) && isset($info['mpeg']['audio']['framelength'])) ? $info['mpeg']['audio']['framelength'] : 0); + + $info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset'] - $xingVBRheaderFrameLength) * 8 / $info['audio']['bitrate']; } $info['audio']['encoder_options'] = $this->GuessEncoderOptions(); @@ -160,10 +185,14 @@ class getid3_mp3 extends getid3_handler return true; } - + /** + * @return string + */ public function GuessEncoderOptions() { // shortcuts $info = &$this->getid3->info; + $thisfile_mpeg_audio = array(); + $thisfile_mpeg_audio_lame = array(); if (!empty($info['mpeg']['audio'])) { $thisfile_mpeg_audio = &$info['mpeg']['audio']; if (!empty($thisfile_mpeg_audio['LAME'])) { @@ -178,7 +207,7 @@ class getid3_mp3 extends getid3_handler $encoder_options = 'VBR q'.$thisfile_mpeg_audio['VBR_quality']; - } elseif (!empty($thisfile_mpeg_audio_lame['preset_used']) && (!in_array($thisfile_mpeg_audio_lame['preset_used_id'], $NamedPresetBitrates))) { + } elseif (!empty($thisfile_mpeg_audio_lame['preset_used']) && isset($thisfile_mpeg_audio_lame['preset_used_id']) && (!in_array($thisfile_mpeg_audio_lame['preset_used_id'], $NamedPresetBitrates))) { $encoder_options = $thisfile_mpeg_audio_lame['preset_used']; @@ -276,7 +305,7 @@ class getid3_mp3 extends getid3_handler } elseif (!empty($info['audio']['bitrate'])) { if ($info['audio']['bitrate_mode'] == 'cbr') { - $encoder_options = strtoupper($info['audio']['bitrate_mode']).ceil($info['audio']['bitrate'] / 1000); + $encoder_options = strtoupper($info['audio']['bitrate_mode']).round($info['audio']['bitrate'] / 1000); } else { $encoder_options = strtoupper($info['audio']['bitrate_mode']); } @@ -286,6 +315,10 @@ class getid3_mp3 extends getid3_handler $encoder_options .= ' -b'.$thisfile_mpeg_audio_lame['bitrate_min']; } + if (isset($thisfile_mpeg_audio['bitrate']) && $thisfile_mpeg_audio['bitrate'] === 'free') { + $encoder_options .= ' --freeformat'; + } + if (!empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev']) || !empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'])) { $encoder_options .= ' --nogap'; } @@ -404,7 +437,15 @@ class getid3_mp3 extends getid3_handler return $encoder_options; } - + /** + * @param int $offset + * @param array $info + * @param bool $recursivesearch + * @param bool $ScanAsCBR + * @param bool $FastMPEGheaderScan + * + * @return bool + */ public function decodeMPEGaudioHeader($offset, &$info, $recursivesearch=true, $ScanAsCBR=false, $FastMPEGheaderScan=false) { static $MPEGaudioVersionLookup; static $MPEGaudioLayerLookup; @@ -458,11 +499,10 @@ class getid3_mp3 extends getid3_handler } $thisfile_mpeg_audio = &$info['mpeg']['audio']; - if ($MPEGaudioHeaderValidCache[$head4_key]) { $thisfile_mpeg_audio['raw'] = $MPEGheaderRawArray; } else { - $this->error('Invalid MPEG audio header ('.getid3_lib::PrintHexBytes($head4).') at offset '.$offset); + $this->warning('Invalid MPEG audio header ('.getid3_lib::PrintHexBytes($head4).') at offset '.$offset); return false; } @@ -562,7 +602,7 @@ class getid3_mp3 extends getid3_handler $thisfile_mpeg_audio['bitrate_mode'] = 'vbr'; $thisfile_mpeg_audio['VBR_method'] = 'Fraunhofer'; - $info['audio']['codec'] = 'Fraunhofer'; + $info['audio']['codec'] = 'Fraunhofer'; $SideInfoData = substr($headerstring, 4 + 2, 32); @@ -655,7 +695,7 @@ class getid3_mp3 extends getid3_handler $used_filesize = $thisfile_mpeg_audio['VBR_bytes']; } elseif (!empty($info['filesize'])) { $used_filesize = $info['filesize']; - $used_filesize -= intval(@$info['id3v2']['headerlength']); + $used_filesize -= (isset($info['id3v2']['headerlength']) ? intval($info['id3v2']['headerlength']) : 0); $used_filesize -= (isset($info['id3v1']) ? 128 : 0); $used_filesize -= (isset($info['tag_offset_end']) ? $info['tag_offset_end'] - $info['tag_offset_start'] : 0); $this->warning('MP3.Xing header missing VBR_bytes, assuming MPEG audio portion of file is '.number_format($used_filesize).' bytes'); @@ -678,7 +718,7 @@ class getid3_mp3 extends getid3_handler if ($thisfile_mpeg_audio['xing_flags']['toc']) { $LAMEtocData = substr($headerstring, $VBRidOffset + 16, 100); for ($i = 0; $i < 100; $i++) { - $thisfile_mpeg_audio['toc'][$i] = ord($LAMEtocData{$i}); + $thisfile_mpeg_audio['toc'][$i] = ord($LAMEtocData[$i]); } } if ($thisfile_mpeg_audio['xing_flags']['vbr_scale']) { @@ -697,187 +737,200 @@ class getid3_mp3 extends getid3_handler $thisfile_mpeg_audio_lame['long_version'] = substr($headerstring, $VBRidOffset + 120, 20); $thisfile_mpeg_audio_lame['short_version'] = substr($thisfile_mpeg_audio_lame['long_version'], 0, 9); - if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.90') { - - // extra 11 chars are not part of version string when LAMEtag present - unset($thisfile_mpeg_audio_lame['long_version']); - - // It the LAME tag was only introduced in LAME v3.90 - // http://www.hydrogenaudio.org/?act=ST&f=15&t=9933 - - // Offsets of various bytes in http://gabriel.mp3-tech.org/mp3infotag.html - // are assuming a 'Xing' identifier offset of 0x24, which is the case for - // MPEG-1 non-mono, but not for other combinations - $LAMEtagOffsetContant = $VBRidOffset - 0x24; - - // shortcuts - $thisfile_mpeg_audio_lame['RGAD'] = array('track'=>array(), 'album'=>array()); - $thisfile_mpeg_audio_lame_RGAD = &$thisfile_mpeg_audio_lame['RGAD']; - $thisfile_mpeg_audio_lame_RGAD_track = &$thisfile_mpeg_audio_lame_RGAD['track']; - $thisfile_mpeg_audio_lame_RGAD_album = &$thisfile_mpeg_audio_lame_RGAD['album']; - $thisfile_mpeg_audio_lame['raw'] = array(); - $thisfile_mpeg_audio_lame_raw = &$thisfile_mpeg_audio_lame['raw']; - - // byte $9B VBR Quality - // This field is there to indicate a quality level, although the scale was not precised in the original Xing specifications. - // Actually overwrites original Xing bytes - unset($thisfile_mpeg_audio['VBR_scale']); - $thisfile_mpeg_audio_lame['vbr_quality'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0x9B, 1)); - - // bytes $9C-$A4 Encoder short VersionString - $thisfile_mpeg_audio_lame['short_version'] = substr($headerstring, $LAMEtagOffsetContant + 0x9C, 9); - - // byte $A5 Info Tag revision + VBR method - $LAMEtagRevisionVBRmethod = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA5, 1)); - - $thisfile_mpeg_audio_lame['tag_revision'] = ($LAMEtagRevisionVBRmethod & 0xF0) >> 4; - $thisfile_mpeg_audio_lame_raw['vbr_method'] = $LAMEtagRevisionVBRmethod & 0x0F; - $thisfile_mpeg_audio_lame['vbr_method'] = self::LAMEvbrMethodLookup($thisfile_mpeg_audio_lame_raw['vbr_method']); - $thisfile_mpeg_audio['bitrate_mode'] = substr($thisfile_mpeg_audio_lame['vbr_method'], 0, 3); // usually either 'cbr' or 'vbr', but truncates 'vbr-old / vbr-rh' to 'vbr' - - // byte $A6 Lowpass filter value - $thisfile_mpeg_audio_lame['lowpass_frequency'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA6, 1)) * 100; - - // bytes $A7-$AE Replay Gain - // http://privatewww.essex.ac.uk/~djmrob/replaygain/rg_data_format.html - // bytes $A7-$AA : 32 bit floating point "Peak signal amplitude" - if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.94b') { - // LAME 3.94a16 and later - 9.23 fixed point - // ie 0x0059E2EE / (2^23) = 5890798 / 8388608 = 0.7022378444671630859375 - $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = (float) ((getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4))) / 8388608); - } else { - // LAME 3.94a15 and earlier - 32-bit floating point - // Actually 3.94a16 will fall in here too and be WRONG, but is hard to detect 3.94a16 vs 3.94a15 - $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = getid3_lib::LittleEndian2Float(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4)); - } - if ($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] == 0) { - unset($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); - } else { - $thisfile_mpeg_audio_lame_RGAD['peak_db'] = getid3_lib::RGADamplitude2dB($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); + //$thisfile_mpeg_audio_lame['numeric_version'] = str_replace('LAME', '', $thisfile_mpeg_audio_lame['short_version']); + $thisfile_mpeg_audio_lame['numeric_version'] = ''; + if (preg_match('#^LAME([0-9\\.a-z]*)#', $thisfile_mpeg_audio_lame['long_version'], $matches)) { + $thisfile_mpeg_audio_lame['short_version'] = $matches[0]; + $thisfile_mpeg_audio_lame['numeric_version'] = $matches[1]; + } + if (strlen($thisfile_mpeg_audio_lame['numeric_version']) > 0) { + foreach (explode('.', $thisfile_mpeg_audio_lame['numeric_version']) as $key => $number) { + $thisfile_mpeg_audio_lame['integer_version'][$key] = intval($number); } + //if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.90') { + if ((($thisfile_mpeg_audio_lame['integer_version'][0] * 1000) + $thisfile_mpeg_audio_lame['integer_version'][1]) >= 3090) { // cannot use string version compare, may have "LAME3.90" or "LAME3.100" -- see https://github.com/JamesHeinrich/getID3/issues/207 - $thisfile_mpeg_audio_lame_raw['RGAD_track'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAB, 2)); - $thisfile_mpeg_audio_lame_raw['RGAD_album'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAD, 2)); + // extra 11 chars are not part of version string when LAMEtag present + unset($thisfile_mpeg_audio_lame['long_version']); + // It the LAME tag was only introduced in LAME v3.90 + // https://wiki.hydrogenaud.io/index.php/LAME#VBR_header_and_LAME_tag + // https://hydrogenaud.io/index.php?topic=9933 - if ($thisfile_mpeg_audio_lame_raw['RGAD_track'] != 0) { + // Offsets of various bytes in http://gabriel.mp3-tech.org/mp3infotag.html + // are assuming a 'Xing' identifier offset of 0x24, which is the case for + // MPEG-1 non-mono, but not for other combinations + $LAMEtagOffsetContant = $VBRidOffset - 0x24; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0xE000) >> 13; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x1C00) >> 10; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x0200) >> 9; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x01FF; - $thisfile_mpeg_audio_lame_RGAD_track['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['name']); - $thisfile_mpeg_audio_lame_RGAD_track['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['originator']); - $thisfile_mpeg_audio_lame_RGAD_track['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit']); + // shortcuts + $thisfile_mpeg_audio_lame['RGAD'] = array('track'=>array(), 'album'=>array()); + $thisfile_mpeg_audio_lame_RGAD = &$thisfile_mpeg_audio_lame['RGAD']; + $thisfile_mpeg_audio_lame_RGAD_track = &$thisfile_mpeg_audio_lame_RGAD['track']; + $thisfile_mpeg_audio_lame_RGAD_album = &$thisfile_mpeg_audio_lame_RGAD['album']; + $thisfile_mpeg_audio_lame['raw'] = array(); + $thisfile_mpeg_audio_lame_raw = &$thisfile_mpeg_audio_lame['raw']; - if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { - $info['replay_gain']['track']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; + // byte $9B VBR Quality + // This field is there to indicate a quality level, although the scale was not precised in the original Xing specifications. + // Actually overwrites original Xing bytes + unset($thisfile_mpeg_audio['VBR_scale']); + $thisfile_mpeg_audio_lame['vbr_quality'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0x9B, 1)); + + // bytes $9C-$A4 Encoder short VersionString + $thisfile_mpeg_audio_lame['short_version'] = substr($headerstring, $LAMEtagOffsetContant + 0x9C, 9); + + // byte $A5 Info Tag revision + VBR method + $LAMEtagRevisionVBRmethod = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA5, 1)); + + $thisfile_mpeg_audio_lame['tag_revision'] = ($LAMEtagRevisionVBRmethod & 0xF0) >> 4; + $thisfile_mpeg_audio_lame_raw['vbr_method'] = $LAMEtagRevisionVBRmethod & 0x0F; + $thisfile_mpeg_audio_lame['vbr_method'] = self::LAMEvbrMethodLookup($thisfile_mpeg_audio_lame_raw['vbr_method']); + $thisfile_mpeg_audio['bitrate_mode'] = substr($thisfile_mpeg_audio_lame['vbr_method'], 0, 3); // usually either 'cbr' or 'vbr', but truncates 'vbr-old / vbr-rh' to 'vbr' + + // byte $A6 Lowpass filter value + $thisfile_mpeg_audio_lame['lowpass_frequency'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA6, 1)) * 100; + + // bytes $A7-$AE Replay Gain + // https://web.archive.org/web/20021015212753/http://privatewww.essex.ac.uk/~djmrob/replaygain/rg_data_format.html + // bytes $A7-$AA : 32 bit floating point "Peak signal amplitude" + if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.94b') { + // LAME 3.94a16 and later - 9.23 fixed point + // ie 0x0059E2EE / (2^23) = 5890798 / 8388608 = 0.7022378444671630859375 + $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = (float) ((getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4))) / 8388608); + } else { + // LAME 3.94a15 and earlier - 32-bit floating point + // Actually 3.94a16 will fall in here too and be WRONG, but is hard to detect 3.94a16 vs 3.94a15 + $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = getid3_lib::LittleEndian2Float(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4)); } - $info['replay_gain']['track']['originator'] = $thisfile_mpeg_audio_lame_RGAD_track['originator']; - $info['replay_gain']['track']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_track['gain_db']; - } else { - unset($thisfile_mpeg_audio_lame_RGAD['track']); - } - if ($thisfile_mpeg_audio_lame_raw['RGAD_album'] != 0) { - - $thisfile_mpeg_audio_lame_RGAD_album['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0xE000) >> 13; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x1C00) >> 10; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x0200) >> 9; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x01FF; - $thisfile_mpeg_audio_lame_RGAD_album['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['name']); - $thisfile_mpeg_audio_lame_RGAD_album['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['originator']); - $thisfile_mpeg_audio_lame_RGAD_album['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit']); - - if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { - $info['replay_gain']['album']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; + if ($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] == 0) { + unset($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); + } else { + $thisfile_mpeg_audio_lame_RGAD['peak_db'] = getid3_lib::RGADamplitude2dB($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); } - $info['replay_gain']['album']['originator'] = $thisfile_mpeg_audio_lame_RGAD_album['originator']; - $info['replay_gain']['album']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_album['gain_db']; - } else { - unset($thisfile_mpeg_audio_lame_RGAD['album']); - } - if (empty($thisfile_mpeg_audio_lame_RGAD)) { - unset($thisfile_mpeg_audio_lame['RGAD']); - } + + $thisfile_mpeg_audio_lame_raw['RGAD_track'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAB, 2)); + $thisfile_mpeg_audio_lame_raw['RGAD_album'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAD, 2)); - // byte $AF Encoding flags + ATH Type - $EncodingFlagsATHtype = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAF, 1)); - $thisfile_mpeg_audio_lame['encoding_flags']['nspsytune'] = (bool) ($EncodingFlagsATHtype & 0x10); - $thisfile_mpeg_audio_lame['encoding_flags']['nssafejoint'] = (bool) ($EncodingFlagsATHtype & 0x20); - $thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'] = (bool) ($EncodingFlagsATHtype & 0x40); - $thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev'] = (bool) ($EncodingFlagsATHtype & 0x80); - $thisfile_mpeg_audio_lame['ath_type'] = $EncodingFlagsATHtype & 0x0F; + if ($thisfile_mpeg_audio_lame_raw['RGAD_track'] != 0) { - // byte $B0 if ABR {specified bitrate} else {minimal bitrate} - $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB0, 1)); - if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 2) { // Average BitRate (ABR) - $thisfile_mpeg_audio_lame['bitrate_abr'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; - } elseif ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { // Constant BitRate (CBR) - // ignore - } elseif ($thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] > 0) { // Variable BitRate (VBR) - minimum bitrate - $thisfile_mpeg_audio_lame['bitrate_min'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; - } + $thisfile_mpeg_audio_lame_RGAD_track['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0xE000) >> 13; + $thisfile_mpeg_audio_lame_RGAD_track['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x1C00) >> 10; + $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x0200) >> 9; + $thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x01FF; + $thisfile_mpeg_audio_lame_RGAD_track['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['name']); + $thisfile_mpeg_audio_lame_RGAD_track['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['originator']); + $thisfile_mpeg_audio_lame_RGAD_track['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit']); - // bytes $B1-$B3 Encoder delays - $EncoderDelays = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB1, 3)); - $thisfile_mpeg_audio_lame['encoder_delay'] = ($EncoderDelays & 0xFFF000) >> 12; - $thisfile_mpeg_audio_lame['end_padding'] = $EncoderDelays & 0x000FFF; + if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { + $info['replay_gain']['track']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; + } + $info['replay_gain']['track']['originator'] = $thisfile_mpeg_audio_lame_RGAD_track['originator']; + $info['replay_gain']['track']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_track['gain_db']; + } else { + unset($thisfile_mpeg_audio_lame_RGAD['track']); + } + if ($thisfile_mpeg_audio_lame_raw['RGAD_album'] != 0) { - // byte $B4 Misc - $MiscByte = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB4, 1)); - $thisfile_mpeg_audio_lame_raw['noise_shaping'] = ($MiscByte & 0x03); - $thisfile_mpeg_audio_lame_raw['stereo_mode'] = ($MiscByte & 0x1C) >> 2; - $thisfile_mpeg_audio_lame_raw['not_optimal_quality'] = ($MiscByte & 0x20) >> 5; - $thisfile_mpeg_audio_lame_raw['source_sample_freq'] = ($MiscByte & 0xC0) >> 6; - $thisfile_mpeg_audio_lame['noise_shaping'] = $thisfile_mpeg_audio_lame_raw['noise_shaping']; - $thisfile_mpeg_audio_lame['stereo_mode'] = self::LAMEmiscStereoModeLookup($thisfile_mpeg_audio_lame_raw['stereo_mode']); - $thisfile_mpeg_audio_lame['not_optimal_quality'] = (bool) $thisfile_mpeg_audio_lame_raw['not_optimal_quality']; - $thisfile_mpeg_audio_lame['source_sample_freq'] = self::LAMEmiscSourceSampleFrequencyLookup($thisfile_mpeg_audio_lame_raw['source_sample_freq']); + $thisfile_mpeg_audio_lame_RGAD_album['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0xE000) >> 13; + $thisfile_mpeg_audio_lame_RGAD_album['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x1C00) >> 10; + $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x0200) >> 9; + $thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x01FF; + $thisfile_mpeg_audio_lame_RGAD_album['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['name']); + $thisfile_mpeg_audio_lame_RGAD_album['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['originator']); + $thisfile_mpeg_audio_lame_RGAD_album['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit']); - // byte $B5 MP3 Gain - $thisfile_mpeg_audio_lame_raw['mp3_gain'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB5, 1), false, true); - $thisfile_mpeg_audio_lame['mp3_gain_db'] = (getid3_lib::RGADamplitude2dB(2) / 4) * $thisfile_mpeg_audio_lame_raw['mp3_gain']; - $thisfile_mpeg_audio_lame['mp3_gain_factor'] = pow(2, ($thisfile_mpeg_audio_lame['mp3_gain_db'] / 6)); - - // bytes $B6-$B7 Preset and surround info - $PresetSurroundBytes = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB6, 2)); - // Reserved = ($PresetSurroundBytes & 0xC000); - $thisfile_mpeg_audio_lame_raw['surround_info'] = ($PresetSurroundBytes & 0x3800); - $thisfile_mpeg_audio_lame['surround_info'] = self::LAMEsurroundInfoLookup($thisfile_mpeg_audio_lame_raw['surround_info']); - $thisfile_mpeg_audio_lame['preset_used_id'] = ($PresetSurroundBytes & 0x07FF); - $thisfile_mpeg_audio_lame['preset_used'] = self::LAMEpresetUsedLookup($thisfile_mpeg_audio_lame); - if (!empty($thisfile_mpeg_audio_lame['preset_used_id']) && empty($thisfile_mpeg_audio_lame['preset_used'])) { - $this->warning('Unknown LAME preset used ('.$thisfile_mpeg_audio_lame['preset_used_id'].') - please report to info@getid3.org'); - } - if (($thisfile_mpeg_audio_lame['short_version'] == 'LAME3.90.') && !empty($thisfile_mpeg_audio_lame['preset_used_id'])) { - // this may change if 3.90.4 ever comes out - $thisfile_mpeg_audio_lame['short_version'] = 'LAME3.90.3'; - } - - // bytes $B8-$BB MusicLength - $thisfile_mpeg_audio_lame['audio_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB8, 4)); - $ExpectedNumberOfAudioBytes = (($thisfile_mpeg_audio_lame['audio_bytes'] > 0) ? $thisfile_mpeg_audio_lame['audio_bytes'] : $thisfile_mpeg_audio['VBR_bytes']); - - // bytes $BC-$BD MusicCRC - $thisfile_mpeg_audio_lame['music_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBC, 2)); - - // bytes $BE-$BF CRC-16 of Info Tag - $thisfile_mpeg_audio_lame['lame_tag_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBE, 2)); + if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { + $info['replay_gain']['album']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; + } + $info['replay_gain']['album']['originator'] = $thisfile_mpeg_audio_lame_RGAD_album['originator']; + $info['replay_gain']['album']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_album['gain_db']; + } else { + unset($thisfile_mpeg_audio_lame_RGAD['album']); + } + if (empty($thisfile_mpeg_audio_lame_RGAD)) { + unset($thisfile_mpeg_audio_lame['RGAD']); + } - // LAME CBR - if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { + // byte $AF Encoding flags + ATH Type + $EncodingFlagsATHtype = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAF, 1)); + $thisfile_mpeg_audio_lame['encoding_flags']['nspsytune'] = (bool) ($EncodingFlagsATHtype & 0x10); + $thisfile_mpeg_audio_lame['encoding_flags']['nssafejoint'] = (bool) ($EncodingFlagsATHtype & 0x20); + $thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'] = (bool) ($EncodingFlagsATHtype & 0x40); + $thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev'] = (bool) ($EncodingFlagsATHtype & 0x80); + $thisfile_mpeg_audio_lame['ath_type'] = $EncodingFlagsATHtype & 0x0F; - $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; - $thisfile_mpeg_audio['bitrate'] = self::ClosestStandardMP3Bitrate($thisfile_mpeg_audio['bitrate']); - $info['audio']['bitrate'] = $thisfile_mpeg_audio['bitrate']; - //if (empty($thisfile_mpeg_audio['bitrate']) || (!empty($thisfile_mpeg_audio_lame['bitrate_min']) && ($thisfile_mpeg_audio_lame['bitrate_min'] != 255))) { - // $thisfile_mpeg_audio['bitrate'] = $thisfile_mpeg_audio_lame['bitrate_min']; - //} + // byte $B0 if ABR {specified bitrate} else {minimal bitrate} + $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB0, 1)); + if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 2) { // Average BitRate (ABR) + $thisfile_mpeg_audio_lame['bitrate_abr'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; + } elseif ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { // Constant BitRate (CBR) + // ignore + } elseif ($thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] > 0) { // Variable BitRate (VBR) - minimum bitrate + $thisfile_mpeg_audio_lame['bitrate_min'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; + } + + // bytes $B1-$B3 Encoder delays + $EncoderDelays = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB1, 3)); + $thisfile_mpeg_audio_lame['encoder_delay'] = ($EncoderDelays & 0xFFF000) >> 12; + $thisfile_mpeg_audio_lame['end_padding'] = $EncoderDelays & 0x000FFF; + + // byte $B4 Misc + $MiscByte = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB4, 1)); + $thisfile_mpeg_audio_lame_raw['noise_shaping'] = ($MiscByte & 0x03); + $thisfile_mpeg_audio_lame_raw['stereo_mode'] = ($MiscByte & 0x1C) >> 2; + $thisfile_mpeg_audio_lame_raw['not_optimal_quality'] = ($MiscByte & 0x20) >> 5; + $thisfile_mpeg_audio_lame_raw['source_sample_freq'] = ($MiscByte & 0xC0) >> 6; + $thisfile_mpeg_audio_lame['noise_shaping'] = $thisfile_mpeg_audio_lame_raw['noise_shaping']; + $thisfile_mpeg_audio_lame['stereo_mode'] = self::LAMEmiscStereoModeLookup($thisfile_mpeg_audio_lame_raw['stereo_mode']); + $thisfile_mpeg_audio_lame['not_optimal_quality'] = (bool) $thisfile_mpeg_audio_lame_raw['not_optimal_quality']; + $thisfile_mpeg_audio_lame['source_sample_freq'] = self::LAMEmiscSourceSampleFrequencyLookup($thisfile_mpeg_audio_lame_raw['source_sample_freq']); + + // byte $B5 MP3 Gain + $thisfile_mpeg_audio_lame_raw['mp3_gain'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB5, 1), false, true); + $thisfile_mpeg_audio_lame['mp3_gain_db'] = (getid3_lib::RGADamplitude2dB(2) / 4) * $thisfile_mpeg_audio_lame_raw['mp3_gain']; + $thisfile_mpeg_audio_lame['mp3_gain_factor'] = pow(2, ($thisfile_mpeg_audio_lame['mp3_gain_db'] / 6)); + + // bytes $B6-$B7 Preset and surround info + $PresetSurroundBytes = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB6, 2)); + // Reserved = ($PresetSurroundBytes & 0xC000); + $thisfile_mpeg_audio_lame_raw['surround_info'] = ($PresetSurroundBytes & 0x3800); + $thisfile_mpeg_audio_lame['surround_info'] = self::LAMEsurroundInfoLookup($thisfile_mpeg_audio_lame_raw['surround_info']); + $thisfile_mpeg_audio_lame['preset_used_id'] = ($PresetSurroundBytes & 0x07FF); + $thisfile_mpeg_audio_lame['preset_used'] = self::LAMEpresetUsedLookup($thisfile_mpeg_audio_lame); + if (!empty($thisfile_mpeg_audio_lame['preset_used_id']) && empty($thisfile_mpeg_audio_lame['preset_used'])) { + $this->warning('Unknown LAME preset used ('.$thisfile_mpeg_audio_lame['preset_used_id'].') - please report to info@getid3.org'); + } + if (($thisfile_mpeg_audio_lame['short_version'] == 'LAME3.90.') && !empty($thisfile_mpeg_audio_lame['preset_used_id'])) { + // this may change if 3.90.4 ever comes out + $thisfile_mpeg_audio_lame['short_version'] = 'LAME3.90.3'; + } + + // bytes $B8-$BB MusicLength + $thisfile_mpeg_audio_lame['audio_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB8, 4)); + $ExpectedNumberOfAudioBytes = (($thisfile_mpeg_audio_lame['audio_bytes'] > 0) ? $thisfile_mpeg_audio_lame['audio_bytes'] : $thisfile_mpeg_audio['VBR_bytes']); + + // bytes $BC-$BD MusicCRC + $thisfile_mpeg_audio_lame['music_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBC, 2)); + + // bytes $BE-$BF CRC-16 of Info Tag + $thisfile_mpeg_audio_lame['lame_tag_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBE, 2)); + + + // LAME CBR + if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1 && $thisfile_mpeg_audio['bitrate'] !== 'free') { + + $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; + $thisfile_mpeg_audio['bitrate'] = self::ClosestStandardMP3Bitrate($thisfile_mpeg_audio['bitrate']); + $info['audio']['bitrate'] = $thisfile_mpeg_audio['bitrate']; + //if (empty($thisfile_mpeg_audio['bitrate']) || (!empty($thisfile_mpeg_audio_lame['bitrate_min']) && ($thisfile_mpeg_audio_lame['bitrate_min'] != 255))) { + // $thisfile_mpeg_audio['bitrate'] = $thisfile_mpeg_audio_lame['bitrate_min']; + //} + + } } - } } @@ -974,6 +1027,22 @@ class getid3_mp3 extends getid3_handler if (!$this->RecursiveFrameScanning($offset, $nextframetestoffset, $ScanAsCBR)) { return false; } + if (!empty($this->getid3->info['mp3_validity_check_bitrates']) && !empty($thisfile_mpeg_audio['bitrate_mode']) && ($thisfile_mpeg_audio['bitrate_mode'] == 'vbr') && !empty($thisfile_mpeg_audio['VBR_bitrate'])) { + // https://github.com/JamesHeinrich/getID3/issues/287 + if (count(array_keys($this->getid3->info['mp3_validity_check_bitrates'])) == 1) { + list($cbr_bitrate_in_short_scan) = array_keys($this->getid3->info['mp3_validity_check_bitrates']); + $deviation_cbr_from_header_bitrate = abs($thisfile_mpeg_audio['VBR_bitrate'] - $cbr_bitrate_in_short_scan) / $cbr_bitrate_in_short_scan; + if ($deviation_cbr_from_header_bitrate < 0.01) { + // VBR header bitrate may differ slightly from true bitrate of frames, perhaps accounting for overhead of VBR header frame itself? + // If measured CBR bitrate is within 1% of specified bitrate in VBR header then assume that file is truly CBR + $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; + //$this->warning('VBR header ignored, assuming CBR '.round($cbr_bitrate_in_short_scan / 1000).'kbps based on scan of '.$this->mp3_valid_check_frames.' frames'); + } + } + } + if (isset($this->getid3->info['mp3_validity_check_bitrates'])) { + unset($this->getid3->info['mp3_validity_check_bitrates']); + } } @@ -1083,13 +1152,21 @@ class getid3_mp3 extends getid3_handler return true; } + /** + * @param int $offset + * @param int $nextframetestoffset + * @param bool $ScanAsCBR + * + * @return bool + */ public function RecursiveFrameScanning(&$offset, &$nextframetestoffset, $ScanAsCBR) { $info = &$this->getid3->info; $firstframetestarray = array('error' => array(), 'warning'=> array(), 'avdataend' => $info['avdataend'], 'avdataoffset' => $info['avdataoffset']); $this->decodeMPEGaudioHeader($offset, $firstframetestarray, false); - for ($i = 0; $i < GETID3_MP3_VALID_CHECK_FRAMES; $i++) { - // check next GETID3_MP3_VALID_CHECK_FRAMES frames for validity, to make sure we haven't run across a false synch + $info['mp3_validity_check_bitrates'] = array(); + for ($i = 0; $i < $this->mp3_valid_check_frames; $i++) { + // check next (default: 50) frames for validity, to make sure we haven't run across a false synch if (($nextframetestoffset + 4) >= $info['avdataend']) { // end of file return true; @@ -1097,6 +1174,8 @@ class getid3_mp3 extends getid3_handler $nextframetestarray = array('error' => array(), 'warning' => array(), 'avdataend' => $info['avdataend'], 'avdataoffset'=>$info['avdataoffset']); if ($this->decodeMPEGaudioHeader($nextframetestoffset, $nextframetestarray, false)) { + /** @phpstan-ignore-next-line */ + getid3_lib::safe_inc($info['mp3_validity_check_bitrates'][$nextframetestarray['mpeg']['audio']['bitrate']]); if ($ScanAsCBR) { // force CBR mode, used for trying to pick out invalid audio streams with valid(?) VBR headers, or VBR streams with no VBR header if (!isset($nextframetestarray['mpeg']['audio']['bitrate']) || !isset($firstframetestarray['mpeg']['audio']['bitrate']) || ($nextframetestarray['mpeg']['audio']['bitrate'] != $firstframetestarray['mpeg']['audio']['bitrate'])) { @@ -1129,6 +1208,12 @@ class getid3_mp3 extends getid3_handler return true; } + /** + * @param int $offset + * @param bool $deepscan + * + * @return int|false + */ public function FreeFormatFrameLength($offset, $deepscan=false) { $info = &$this->getid3->info; @@ -1137,9 +1222,9 @@ class getid3_mp3 extends getid3_handler $SyncPattern1 = substr($MPEGaudioData, 0, 4); // may be different pattern due to padding - $SyncPattern2 = $SyncPattern1{0}.$SyncPattern1{1}.chr(ord($SyncPattern1{2}) | 0x02).$SyncPattern1{3}; + $SyncPattern2 = $SyncPattern1[0].$SyncPattern1[1].chr(ord($SyncPattern1[2]) | 0x02).$SyncPattern1[3]; if ($SyncPattern2 === $SyncPattern1) { - $SyncPattern2 = $SyncPattern1{0}.$SyncPattern1{1}.chr(ord($SyncPattern1{2}) & 0xFD).$SyncPattern1{3}; + $SyncPattern2 = $SyncPattern1[0].$SyncPattern1[1].chr(ord($SyncPattern1[2]) & 0xFD).$SyncPattern1[3]; } $framelength = false; @@ -1206,6 +1291,9 @@ class getid3_mp3 extends getid3_handler return $framelength; } + /** + * @return bool + */ public function getOnlyMPEGaudioInfoBruteForce() { $MPEGaudioHeaderDecodeCache = array(); $MPEGaudioHeaderValidCache = array(); @@ -1222,6 +1310,7 @@ class getid3_mp3 extends getid3_handler $LongMPEGbitrateLookup = array(); $LongMPEGpaddingLookup = array(); $LongMPEGfrequencyLookup = array(); + $Distribution = array(); $Distribution['bitrate'] = array(); $Distribution['frequency'] = array(); $Distribution['layer'] = array(); @@ -1241,9 +1330,9 @@ class getid3_mp3 extends getid3_handler if (strlen($head4) < 4) { break; } - if ($head4{0} != "\xFF") { + if ($head4[0] != "\xFF") { for ($i = 1; $i < 4; $i++) { - if ($head4{$i} == "\xFF") { + if ($head4[$i] == "\xFF") { $this->fseek($i - 4, SEEK_CUR); continue 2; } @@ -1275,7 +1364,7 @@ class getid3_mp3 extends getid3_handler $WhereWeWere = $this->ftell(); $this->fseek($MPEGaudioHeaderLengthCache[$head4] - 4, SEEK_CUR); $next4 = $this->fread(4); - if ($next4{0} == "\xFF") { + if ($next4[0] == "\xFF") { if (!isset($MPEGaudioHeaderDecodeCache[$next4])) { $MPEGaudioHeaderDecodeCache[$next4] = self::MPEGaudioHeaderDecode($next4); } @@ -1285,17 +1374,17 @@ class getid3_mp3 extends getid3_handler if ($MPEGaudioHeaderValidCache[$next4]) { $this->fseek(-4, SEEK_CUR); - getid3_lib::safe_inc($Distribution['bitrate'][$LongMPEGbitrateLookup[$head4]]); - getid3_lib::safe_inc($Distribution['layer'][$LongMPEGlayerLookup[$head4]]); - getid3_lib::safe_inc($Distribution['version'][$LongMPEGversionLookup[$head4]]); - getid3_lib::safe_inc($Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])]); - getid3_lib::safe_inc($Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]]); - if ($max_frames_scan && (++$frames_scanned >= $max_frames_scan)) { - $pct_data_scanned = ($this->ftell() - $info['avdataoffset']) / ($info['avdataend'] - $info['avdataoffset']); + $Distribution['bitrate'][$LongMPEGbitrateLookup[$head4]] = isset($Distribution['bitrate'][$LongMPEGbitrateLookup[$head4]]) ? ++$Distribution['bitrate'][$LongMPEGbitrateLookup[$head4]] : 1; + $Distribution['layer'][$LongMPEGlayerLookup[$head4]] = isset($Distribution['layer'][$LongMPEGlayerLookup[$head4]]) ? ++$Distribution['layer'][$LongMPEGlayerLookup[$head4]] : 1; + $Distribution['version'][$LongMPEGversionLookup[$head4]] = isset($Distribution['version'][$LongMPEGversionLookup[$head4]]) ? ++$Distribution['version'][$LongMPEGversionLookup[$head4]] : 1; + $Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])] = isset($Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])]) ? ++$Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])] : 1; + $Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]] = isset($Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]]) ? ++$Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]] : 1; + if (++$frames_scanned >= $max_frames_scan) { + $pct_data_scanned = getid3_lib::SafeDiv($this->ftell() - $info['avdataoffset'], $info['avdataend'] - $info['avdataoffset']); $this->warning('too many MPEG audio frames to scan, only scanned first '.$max_frames_scan.' frames ('.number_format($pct_data_scanned * 100, 1).'% of file) and extrapolated distribution, playtime and bitrate may be incorrect.'); foreach ($Distribution as $key1 => $value1) { foreach ($value1 as $key2 => $value2) { - $Distribution[$key1][$key2] = round($value2 / $pct_data_scanned); + $Distribution[$key1][$key2] = $pct_data_scanned ? round($value2 / $pct_data_scanned) : 1; } } break; @@ -1353,7 +1442,12 @@ class getid3_mp3 extends getid3_handler return true; } - + /** + * @param int $avdataoffset + * @param bool $BitrateHistogram + * + * @return bool + */ public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { // looks for synch, decodes MPEG audio header @@ -1363,10 +1457,9 @@ class getid3_mp3 extends getid3_handler static $MPEGaudioLayerLookup; static $MPEGaudioBitrateLookup; if (empty($MPEGaudioVersionLookup)) { - $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); - $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); - $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); - + $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); + $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); + $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); } $this->fseek($avdataoffset); @@ -1378,8 +1471,11 @@ class getid3_mp3 extends getid3_handler $header = $this->fread($sync_seek_buffer_size); $sync_seek_buffer_size = strlen($header); $SynchSeekOffset = 0; + $SyncSeekAttempts = 0; + $SyncSeekAttemptsMax = 1000; + $FirstFrameThisfileInfo = null; while ($SynchSeekOffset < $sync_seek_buffer_size) { - if ((($avdataoffset + $SynchSeekOffset) < $info['avdataend']) && !feof($this->getid3->fp)) { + if ((($avdataoffset + $SynchSeekOffset) < $info['avdataend']) && !$this->feof()) { if ($SynchSeekOffset > $sync_seek_buffer_size) { // if a synch's not found within the first 128k bytes, then give up @@ -1394,20 +1490,6 @@ class getid3_mp3 extends getid3_handler unset($info['mpeg']); } return false; - - } elseif (feof($this->getid3->fp)) { - - $this->error('Could not find valid MPEG audio synch before end of file'); - if (isset($info['audio']['bitrate'])) { - unset($info['audio']['bitrate']); - } - if (isset($info['mpeg']['audio'])) { - unset($info['mpeg']['audio']); - } - if (isset($info['mpeg']) && (!is_array($info['mpeg']) || (count($info['mpeg']) == 0))) { - unset($info['mpeg']); - } - return false; } } @@ -1416,7 +1498,25 @@ class getid3_mp3 extends getid3_handler return false; } - if (($header{$SynchSeekOffset} == "\xFF") && ($header{($SynchSeekOffset + 1)} > "\xE0")) { // synch detected + if (($header[$SynchSeekOffset] == "\xFF") && ($header[($SynchSeekOffset + 1)] > "\xE0")) { // possible synch detected + if (++$SyncSeekAttempts >= $SyncSeekAttemptsMax) { + // https://github.com/JamesHeinrich/getID3/issues/286 + // corrupt files claiming to be MP3, with a large number of 0xFF bytes near the beginning, can cause this loop to take a very long time + // should have escape condition to avoid spending too much time scanning a corrupt file + // if a synch's not found within the first 128k bytes, then give up + $this->error('Could not find valid MPEG audio synch after scanning '.$SyncSeekAttempts.' candidate offsets'); + if (isset($info['audio']['bitrate'])) { + unset($info['audio']['bitrate']); + } + if (isset($info['mpeg']['audio'])) { + unset($info['mpeg']['audio']); + } + if (empty($info['mpeg'])) { + unset($info['mpeg']); + } + return false; + } + $FirstFrameAVDataOffset = null; if (!isset($FirstFrameThisfileInfo) && !isset($info['mpeg']['audio'])) { $FirstFrameThisfileInfo = $info; $FirstFrameAVDataOffset = $avdataoffset + $SynchSeekOffset; @@ -1440,7 +1540,7 @@ class getid3_mp3 extends getid3_handler $info['audio']['dataformat'] = 'mp3'; break; } - if (isset($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode']) && ($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode'] == 'vbr')) { + if (isset($FirstFrameThisfileInfo) && isset($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode']) && ($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode'] == 'vbr')) { if (!(abs($info['audio']['bitrate'] - $FirstFrameThisfileInfo['audio']['bitrate']) <= 1)) { // If there is garbage data between a valid VBR header frame and a sequence // of valid MPEG-audio frames the VBR data is no longer discarded. @@ -1455,9 +1555,9 @@ class getid3_mp3 extends getid3_handler if ($this->decodeMPEGaudioHeader($GarbageOffsetEnd, $dummy, true, true)) { $info = $dummy; $info['avdataoffset'] = $GarbageOffsetEnd; - $this->warning('apparently-valid VBR header not used because could not find '.GETID3_MP3_VALID_CHECK_FRAMES.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.'), but did find valid CBR stream starting at '.$GarbageOffsetEnd); + $this->warning('apparently-valid VBR header not used because could not find '.$this->mp3_valid_check_frames.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.'), but did find valid CBR stream starting at '.$GarbageOffsetEnd); } else { - $this->warning('using data from VBR header even though could not find '.GETID3_MP3_VALID_CHECK_FRAMES.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.')'); + $this->warning('using data from VBR header even though could not find '.$this->mp3_valid_check_frames.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.')'); } } } @@ -1502,6 +1602,7 @@ class getid3_mp3 extends getid3_handler $pct_data_scanned = 0; for ($current_segment = 0; $current_segment < $max_scan_segments; $current_segment++) { $frames_scanned_this_segment = 0; + $scan_start_offset = array(); if ($this->ftell() >= $info['avdataend']) { break; } @@ -1510,7 +1611,7 @@ class getid3_mp3 extends getid3_handler $this->fseek($scan_start_offset[$current_segment]); $buffer_4k = $this->fread(4096); for ($j = 0; $j < (strlen($buffer_4k) - 4); $j++) { - if (($buffer_4k{$j} == "\xFF") && ($buffer_4k{($j + 1)} > "\xE0")) { // synch detected + if (($buffer_4k[$j] == "\xFF") && ($buffer_4k[($j + 1)] > "\xE0")) { // synch detected if ($this->decodeMPEGaudioHeader($scan_start_offset[$current_segment] + $j, $dummy, false, false, $FastMode)) { $calculated_next_offset = $scan_start_offset[$current_segment] + $j + $dummy['mpeg']['audio']['framelength']; if ($this->decodeMPEGaudioHeader($calculated_next_offset, $dummy, false, false, $FastMode)) { @@ -1522,7 +1623,7 @@ class getid3_mp3 extends getid3_handler } } $synchstartoffset = $scan_start_offset[$current_segment]; - while ($this->decodeMPEGaudioHeader($synchstartoffset, $dummy, false, false, $FastMode)) { + while (($synchstartoffset < $info['avdataend']) && $this->decodeMPEGaudioHeader($synchstartoffset, $dummy, false, false, $FastMode)) { $FastMode = true; $thisframebitrate = $MPEGaudioBitrateLookup[$MPEGaudioVersionLookup[$dummy['mpeg']['audio']['raw']['version']]][$MPEGaudioLayerLookup[$dummy['mpeg']['audio']['raw']['layer']]][$dummy['mpeg']['audio']['raw']['bitrate']]; @@ -1537,7 +1638,7 @@ class getid3_mp3 extends getid3_handler } $frames_scanned++; if ($frames_scan_per_segment && (++$frames_scanned_this_segment >= $frames_scan_per_segment)) { - $this_pct_scanned = ($this->ftell() - $scan_start_offset[$current_segment]) / ($info['avdataend'] - $info['avdataoffset']); + $this_pct_scanned = getid3_lib::SafeDiv($this->ftell() - $scan_start_offset[$current_segment], $info['avdataend'] - $info['avdataoffset']); if (($current_segment == 0) && (($this_pct_scanned * $max_scan_segments) >= 1)) { // file likely contains < $max_frames_scan, just scan as one segment $max_scan_segments = 1; @@ -1628,22 +1729,34 @@ class getid3_mp3 extends getid3_handler } $info['audio']['channels'] = $info['mpeg']['audio']['channels']; + if ($info['audio']['channels'] < 1) { + $this->error('Corrupt MP3 file: no channels'); + return false; + } $info['audio']['channelmode'] = $info['mpeg']['audio']['channelmode']; $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; return true; } - + /** + * @return array + */ public static function MPEGaudioVersionArray() { static $MPEGaudioVersion = array('2.5', false, '2', '1'); return $MPEGaudioVersion; } + /** + * @return array + */ public static function MPEGaudioLayerArray() { static $MPEGaudioLayer = array(false, 3, 2, 1); return $MPEGaudioLayer; } + /** + * @return array + */ public static function MPEGaudioBitrateArray() { static $MPEGaudioBitrate; if (empty($MPEGaudioBitrate)) { @@ -1663,6 +1776,9 @@ class getid3_mp3 extends getid3_handler return $MPEGaudioBitrate; } + /** + * @return array + */ public static function MPEGaudioFrequencyArray() { static $MPEGaudioFrequency; if (empty($MPEGaudioFrequency)) { @@ -1675,11 +1791,17 @@ class getid3_mp3 extends getid3_handler return $MPEGaudioFrequency; } + /** + * @return array + */ public static function MPEGaudioChannelModeArray() { static $MPEGaudioChannelMode = array('stereo', 'joint stereo', 'dual channel', 'mono'); return $MPEGaudioChannelMode; } + /** + * @return array + */ public static function MPEGaudioModeExtensionArray() { static $MPEGaudioModeExtension; if (empty($MPEGaudioModeExtension)) { @@ -1692,17 +1814,33 @@ class getid3_mp3 extends getid3_handler return $MPEGaudioModeExtension; } + /** + * @return array + */ public static function MPEGaudioEmphasisArray() { static $MPEGaudioEmphasis = array('none', '50/15ms', false, 'CCIT J.17'); return $MPEGaudioEmphasis; } + /** + * @param string $head4 + * @param bool $allowBitrate15 + * + * @return bool + */ public static function MPEGaudioHeaderBytesValid($head4, $allowBitrate15=false) { return self::MPEGaudioHeaderValid(self::MPEGaudioHeaderDecode($head4), false, $allowBitrate15); } + /** + * @param array $rawarray + * @param bool $echoerrors + * @param bool $allowBitrate15 + * + * @return bool + */ public static function MPEGaudioHeaderValid($rawarray, $echoerrors=false, $allowBitrate15=false) { - if (($rawarray['synch'] & 0x0FFE) != 0x0FFE) { + if (!isset($rawarray['synch']) || ($rawarray['synch'] & 0x0FFE) != 0x0FFE) { return false; } @@ -1773,6 +1911,11 @@ class getid3_mp3 extends getid3_handler return true; } + /** + * @param string $Header4Bytes + * + * @return array|false + */ public static function MPEGaudioHeaderDecode($Header4Bytes) { // AAAA AAAA AAAB BCCD EEEE FFGH IIJJ KLMM // A - Frame sync (all bits set) @@ -1793,23 +1936,33 @@ class getid3_mp3 extends getid3_handler return false; } + $MPEGrawHeader = array(); $MPEGrawHeader['synch'] = (getid3_lib::BigEndian2Int(substr($Header4Bytes, 0, 2)) & 0xFFE0) >> 4; - $MPEGrawHeader['version'] = (ord($Header4Bytes{1}) & 0x18) >> 3; // BB - $MPEGrawHeader['layer'] = (ord($Header4Bytes{1}) & 0x06) >> 1; // CC - $MPEGrawHeader['protection'] = (ord($Header4Bytes{1}) & 0x01); // D - $MPEGrawHeader['bitrate'] = (ord($Header4Bytes{2}) & 0xF0) >> 4; // EEEE - $MPEGrawHeader['sample_rate'] = (ord($Header4Bytes{2}) & 0x0C) >> 2; // FF - $MPEGrawHeader['padding'] = (ord($Header4Bytes{2}) & 0x02) >> 1; // G - $MPEGrawHeader['private'] = (ord($Header4Bytes{2}) & 0x01); // H - $MPEGrawHeader['channelmode'] = (ord($Header4Bytes{3}) & 0xC0) >> 6; // II - $MPEGrawHeader['modeextension'] = (ord($Header4Bytes{3}) & 0x30) >> 4; // JJ - $MPEGrawHeader['copyright'] = (ord($Header4Bytes{3}) & 0x08) >> 3; // K - $MPEGrawHeader['original'] = (ord($Header4Bytes{3}) & 0x04) >> 2; // L - $MPEGrawHeader['emphasis'] = (ord($Header4Bytes{3}) & 0x03); // MM + $MPEGrawHeader['version'] = (ord($Header4Bytes[1]) & 0x18) >> 3; // BB + $MPEGrawHeader['layer'] = (ord($Header4Bytes[1]) & 0x06) >> 1; // CC + $MPEGrawHeader['protection'] = (ord($Header4Bytes[1]) & 0x01); // D + $MPEGrawHeader['bitrate'] = (ord($Header4Bytes[2]) & 0xF0) >> 4; // EEEE + $MPEGrawHeader['sample_rate'] = (ord($Header4Bytes[2]) & 0x0C) >> 2; // FF + $MPEGrawHeader['padding'] = (ord($Header4Bytes[2]) & 0x02) >> 1; // G + $MPEGrawHeader['private'] = (ord($Header4Bytes[2]) & 0x01); // H + $MPEGrawHeader['channelmode'] = (ord($Header4Bytes[3]) & 0xC0) >> 6; // II + $MPEGrawHeader['modeextension'] = (ord($Header4Bytes[3]) & 0x30) >> 4; // JJ + $MPEGrawHeader['copyright'] = (ord($Header4Bytes[3]) & 0x08) >> 3; // K + $MPEGrawHeader['original'] = (ord($Header4Bytes[3]) & 0x04) >> 2; // L + $MPEGrawHeader['emphasis'] = (ord($Header4Bytes[3]) & 0x03); // MM return $MPEGrawHeader; } + /** + * @param int|string $bitrate + * @param string $version + * @param string $layer + * @param bool $padding + * @param int $samplerate + * + * @return int|false + */ public static function MPEGaudioFrameLength(&$bitrate, &$version, &$layer, $padding, &$samplerate) { static $AudioFrameLengthCache = array(); @@ -1871,6 +2024,11 @@ class getid3_mp3 extends getid3_handler return $AudioFrameLengthCache[$bitrate][$version][$layer][$padding][$samplerate]; } + /** + * @param float|int $bit_rate + * + * @return int|float|string + */ public static function ClosestStandardMP3Bitrate($bit_rate) { static $standard_bit_rates = array (320000, 256000, 224000, 192000, 160000, 128000, 112000, 96000, 80000, 64000, 56000, 48000, 40000, 32000, 24000, 16000, 8000); static $bit_rate_table = array (0=>'-'); @@ -1891,10 +2049,16 @@ class getid3_mp3 extends getid3_handler return $bit_rate_table[$round_bit_rate]; } + /** + * @param string $version + * @param string $channelmode + * + * @return int + */ public static function XingVBRidOffset($version, $channelmode) { static $XingVBRidOffsetCache = array(); - if (empty($XingVBRidOffset)) { - $XingVBRidOffset = array ( + if (empty($XingVBRidOffsetCache)) { + $XingVBRidOffsetCache = array ( '1' => array ('mono' => 0x15, // 4 + 17 = 21 'stereo' => 0x24, // 4 + 32 = 36 'joint stereo' => 0x24, @@ -1914,9 +2078,14 @@ class getid3_mp3 extends getid3_handler ) ); } - return $XingVBRidOffset[$version][$channelmode]; + return $XingVBRidOffsetCache[$version][$channelmode]; } + /** + * @param int $VBRmethodID + * + * @return string + */ public static function LAMEvbrMethodLookup($VBRmethodID) { static $LAMEvbrMethodLookup = array( 0x00 => 'unknown', @@ -1933,6 +2102,11 @@ class getid3_mp3 extends getid3_handler return (isset($LAMEvbrMethodLookup[$VBRmethodID]) ? $LAMEvbrMethodLookup[$VBRmethodID] : ''); } + /** + * @param int $StereoModeID + * + * @return string + */ public static function LAMEmiscStereoModeLookup($StereoModeID) { static $LAMEmiscStereoModeLookup = array( 0 => 'mono', @@ -1947,6 +2121,11 @@ class getid3_mp3 extends getid3_handler return (isset($LAMEmiscStereoModeLookup[$StereoModeID]) ? $LAMEmiscStereoModeLookup[$StereoModeID] : ''); } + /** + * @param int $SourceSampleFrequencyID + * + * @return string + */ public static function LAMEmiscSourceSampleFrequencyLookup($SourceSampleFrequencyID) { static $LAMEmiscSourceSampleFrequencyLookup = array( 0 => '<= 32 kHz', @@ -1957,6 +2136,11 @@ class getid3_mp3 extends getid3_handler return (isset($LAMEmiscSourceSampleFrequencyLookup[$SourceSampleFrequencyID]) ? $LAMEmiscSourceSampleFrequencyLookup[$SourceSampleFrequencyID] : ''); } + /** + * @param int $SurroundInfoID + * + * @return string + */ public static function LAMEsurroundInfoLookup($SurroundInfoID) { static $LAMEsurroundInfoLookup = array( 0 => 'no surround info', @@ -1967,6 +2151,11 @@ class getid3_mp3 extends getid3_handler return (isset($LAMEsurroundInfoLookup[$SurroundInfoID]) ? $LAMEsurroundInfoLookup[$SurroundInfoID] : 'reserved'); } + /** + * @param array $LAMEtag + * + * @return string + */ public static function LAMEpresetUsedLookup($LAMEtag) { if ($LAMEtag['preset_used_id'] == 0) { diff --git a/lib/getid3/module.audio.mpc.php b/lib/getid3/module.audio.mpc.php index 14e829d..f754f4b 100644 --- a/lib/getid3/module.audio.mpc.php +++ b/lib/getid3/module.audio.mpc.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.mpc.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_mpc extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -43,7 +48,7 @@ class getid3_mpc extends getid3_handler // this is SV7 return $this->ParseMPCsv7(); - } elseif (preg_match('/^[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]/s', $MPCheaderData)) { + } elseif (preg_match('#^[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]#s', $MPCheaderData)) { // this is SV4 - SV6, handle seperately return $this->ParseMPCsv6(); @@ -56,10 +61,11 @@ class getid3_mpc extends getid3_handler return false; } - return false; } - + /** + * @return bool + */ public function ParseMPCsv8() { // this is SV8 // http://trac.musepack.net/trac/wiki/SV8Specification @@ -131,7 +137,7 @@ class getid3_mpc extends getid3_handler $info['audio']['channels'] = $thisPacket['channels']; $info['audio']['sample_rate'] = $thisPacket['sample_frequency']; $info['playtime_seconds'] = $thisPacket['sample_count'] / $thisPacket['sample_frequency']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); break; case 'RG': // Replay Gain @@ -197,7 +203,6 @@ class getid3_mpc extends getid3_handler default: $this->error('Found unhandled key type "'.$thisPacket['key'].'" at offset '.$thisPacket['offset']); return false; - break; } if (!empty($thisPacket)) { $info['mpc']['packets'][] = $thisPacket; @@ -208,6 +213,9 @@ class getid3_mpc extends getid3_handler return true; } + /** + * @return bool + */ public function ParseMPCsv7() { // this is SV7 // http://www.uni-jena.de/~pfk/mpp/sv8/header.html @@ -274,7 +282,7 @@ class getid3_mpc extends getid3_handler $info['audio']['sample_rate'] = $thisfile_mpc_header['sample_rate']; $thisfile_mpc_header['samples'] = ((($thisfile_mpc_header['frame_count'] - 1) * 1152) + $thisfile_mpc_header['last_frame_length']) * $info['audio']['channels']; - $info['playtime_seconds'] = ($thisfile_mpc_header['samples'] / $info['audio']['channels']) / $info['audio']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($thisfile_mpc_header['samples'], $info['audio']['channels'] * $info['audio']['sample_rate']); if ($info['playtime_seconds'] == 0) { $this->error('Corrupt MPC file: playtime_seconds == zero'); return false; @@ -322,6 +330,9 @@ class getid3_mpc extends getid3_handler return true; } + /** + * @return bool + */ public function ParseMPCsv6() { // this is SV4 - SV6 @@ -337,6 +348,7 @@ class getid3_mpc extends getid3_handler $info['avdataoffset'] += $thisfile_mpc_header['size']; // Most of this code adapted from Jurgen Faul's MPEGplus source code - thanks Jurgen! :) + $HeaderDWORD = array(); $HeaderDWORD[0] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, 0, 4)); $HeaderDWORD[1] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, 4, 4)); @@ -373,7 +385,6 @@ class getid3_mpc extends getid3_handler $info['error'] = 'Expecting 4, 5 or 6 in version field, found '.$thisfile_mpc_header['stream_version_major'].' instead'; unset($info['mpc']); return false; - break; } if (($thisfile_mpc_header['stream_version_major'] > 4) && ($thisfile_mpc_header['block_size'] != 1)) { @@ -397,7 +408,11 @@ class getid3_mpc extends getid3_handler return true; } - + /** + * @param int $profileid + * + * @return string + */ public function MPCprofileNameLookup($profileid) { static $MPCprofileNameLookup = array( 0 => 'no profile', @@ -420,6 +435,11 @@ class getid3_mpc extends getid3_handler return (isset($MPCprofileNameLookup[$profileid]) ? $MPCprofileNameLookup[$profileid] : 'invalid'); } + /** + * @param int $frequencyid + * + * @return int|string + */ public function MPCfrequencyLookup($frequencyid) { static $MPCfrequencyLookup = array( 0 => 44100, @@ -430,6 +450,11 @@ class getid3_mpc extends getid3_handler return (isset($MPCfrequencyLookup[$frequencyid]) ? $MPCfrequencyLookup[$frequencyid] : 'invalid'); } + /** + * @param int $intvalue + * + * @return float|false + */ public function MPCpeakDBLookup($intvalue) { if ($intvalue > 0) { return ((log10($intvalue) / log10(2)) - 15) * 6; @@ -437,6 +462,11 @@ class getid3_mpc extends getid3_handler return false; } + /** + * @param int $encoderversion + * + * @return string + */ public function MPCencoderVersionLookup($encoderversion) { //Encoder version * 100 (106 = 1.06) //EncoderVersion % 10 == 0 Release (1.0) @@ -464,6 +494,13 @@ class getid3_mpc extends getid3_handler return number_format($encoderversion / 100, 2).' alpha'; } + /** + * @param string $data + * @param int $packetLength + * @param int $maxHandledPacketLength + * + * @return int|false + */ public function SV8variableLengthInteger($data, &$packetLength, $maxHandledPacketLength=9) { $packet_size = 0; for ($packetLength = 1; $packetLength <= $maxHandledPacketLength; $packetLength++) { @@ -488,6 +525,11 @@ class getid3_mpc extends getid3_handler return $packet_size; } + /** + * @param string $packetKey + * + * @return string + */ public function MPCsv8PacketName($packetKey) { static $MPCsv8PacketName = array(); if (empty($MPCsv8PacketName)) { diff --git a/lib/getid3/module.audio.ogg.php b/lib/getid3/module.audio.ogg.php index e41c96c..ebd2b94 100644 --- a/lib/getid3/module.audio.ogg.php +++ b/lib/getid3/module.audio.ogg.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.ogg.php // @@ -14,11 +14,18 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true); class getid3_ogg extends getid3_handler { - // http://xiph.org/vorbis/doc/Vorbis_I_spec.html + /** + * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html + * + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -65,7 +72,7 @@ class getid3_ogg extends getid3_handler } elseif (substr($filedata, 0, 8) == 'OpusHead') { - if( $this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) == false ) { + if ($this->ParseOpusPageHeader($filedata, $filedataoffset, $oggpageinfo) === false) { return false; } @@ -203,8 +210,8 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' $filedataoffset += 20; $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor']; - $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']; - $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']; + $info['ogg']['skeleton']['fishead']['presentationtime'] = getid3_lib::SafeDiv($info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'], $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']); + $info['ogg']['skeleton']['fishead']['basetime'] = getid3_lib::SafeDiv($info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'], $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']); $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc']; @@ -259,9 +266,34 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' $this->error('Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']'); //return false; + } elseif (substr($filedata, 0, 5) == "\x7F".'FLAC') { + // https://xiph.org/flac/ogg_mapping.html + + $info['audio']['dataformat'] = 'flac'; + $info['audio']['bitrate_mode'] = 'vbr'; + $info['audio']['lossless'] = true; + + $info['ogg']['flac']['header']['version_major'] = ord(substr($filedata, 5, 1)); + $info['ogg']['flac']['header']['version_minor'] = ord(substr($filedata, 6, 1)); + $info['ogg']['flac']['header']['header_packets'] = getid3_lib::BigEndian2Int(substr($filedata, 7, 2)) + 1; // "A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one. This number may be zero (0x0000) to signify 'unknown' but be aware that some decoders may not be able to handle such streams." + $info['ogg']['flac']['header']['magic'] = substr($filedata, 9, 4); + if ($info['ogg']['flac']['header']['magic'] != 'fLaC') { + $this->error('Ogg-FLAC expecting "fLaC", found "'.$info['ogg']['flac']['header']['magic'].'" ('.trim(getid3_lib::PrintHexBytes($info['ogg']['flac']['header']['magic'])).')'); + return false; + } + $info['ogg']['flac']['header']['STREAMINFO_bytes'] = getid3_lib::BigEndian2Int(substr($filedata, 13, 4)); + $info['flac']['STREAMINFO'] = getid3_flac::parseSTREAMINFOdata(substr($filedata, 17, 34)); + if (!empty($info['flac']['STREAMINFO']['sample_rate'])) { + $info['audio']['bitrate_mode'] = 'vbr'; + $info['audio']['sample_rate'] = $info['flac']['STREAMINFO']['sample_rate']; + $info['audio']['channels'] = $info['flac']['STREAMINFO']['channels']; + $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['flac']['STREAMINFO']['samples_stream'], $info['flac']['STREAMINFO']['sample_rate']); + } + } else { - $this->error('Expecting either "Speex ", "OpusHead" or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"'); + $this->error('Expecting one of "vorbis", "Speex", "OpusHead", "vorbis", "fishhead", "theora", "fLaC" identifier strings, found "'.substr($filedata, 0, 8).'"'); unset($info['ogg']); unset($info['mime_type']); return false; @@ -327,7 +359,7 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return false; } if (!empty($info['audio']['sample_rate'])) { - $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']); + $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) * $info['audio']['sample_rate'] / $info['ogg']['samples']; } } @@ -378,6 +410,13 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return true; } + /** + * @param string $filedata + * @param int $filedataoffset + * @param array $oggpageinfo + * + * @return bool + */ public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { $info = &$this->getid3->info; $info['audio']['dataformat'] = 'vorbis'; @@ -426,7 +465,15 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return true; } - // http://tools.ietf.org/html/draft-ietf-codec-oggopus-03 + /** + * @link http://tools.ietf.org/html/draft-ietf-codec-oggopus-03 + * + * @param string $filedata + * @param int $filedataoffset + * @param array $oggpageinfo + * + * @return bool + */ public function ParseOpusPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { $info = &$this->getid3->info; $info['audio']['dataformat'] = 'opus'; @@ -458,7 +505,7 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' $info['ogg']['pageheader']['opus']['pre_skip'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); $filedataoffset += 2; - $info['ogg']['pageheader']['opus']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); + $info['ogg']['pageheader']['opus']['input_sample_rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); $filedataoffset += 4; //$info['ogg']['pageheader']['opus']['output_gain'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); @@ -467,29 +514,33 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' //$info['ogg']['pageheader']['opus']['channel_mapping_family'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); //$filedataoffset += 1; - $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version']; - $info['opus']['sample_rate'] = $info['ogg']['pageheader']['opus']['sample_rate']; - $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count']; + $info['opus']['opus_version'] = $info['ogg']['pageheader']['opus']['version']; + $info['opus']['sample_rate_input'] = $info['ogg']['pageheader']['opus']['input_sample_rate']; + $info['opus']['out_channel_count'] = $info['ogg']['pageheader']['opus']['out_channel_count']; - $info['audio']['channels'] = $info['opus']['out_channel_count']; - $info['audio']['sample_rate'] = $info['opus']['sample_rate']; + $info['audio']['channels'] = $info['opus']['out_channel_count']; + $info['audio']['sample_rate_input'] = $info['opus']['sample_rate_input']; + $info['audio']['sample_rate'] = 48000; // "All Opus audio is coded at 48 kHz, and should also be decoded at 48 kHz for playback (unless the target hardware does not support this sampling rate). However, this field may be used to resample the audio back to the original sampling rate, for example, when saving the output to a file." -- https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/structOpusHead.html return true; } - + /** + * @return array|false + */ public function ParseOggPageHeader() { // http://xiph.org/ogg/vorbis/doc/framing.html + $oggheader = array(); $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file $filedata = $this->fread($this->getid3->fread_buffer_size()); $filedataoffset = 0; - while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) { + while (substr($filedata, $filedataoffset++, 4) != 'OggS') { if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) { // should be found before here return false; } - if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) { - if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) { + if (($filedataoffset + 28) > strlen($filedata)) { + if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) { // get some more data, unless eof, in which case fail return false; } @@ -528,13 +579,19 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return $oggheader; } - // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005 + /** + * @link http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005 + * + * @return bool + */ public function ParseVorbisComments() { $info = &$this->getid3->info; $OriginalOffset = $this->ftell(); + $commentdata = null; $commentdataoffset = 0; $VorbisCommentPage = 1; + $CommentStartOffset = 0; switch ($info['audio']['dataformat']) { case 'vorbis': @@ -562,7 +619,6 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' default: return false; - break; } $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4)); @@ -625,35 +681,39 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' $VorbisCommentPage++; - $oggpageinfo = $this->ParseOggPageHeader(); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; + if ($oggpageinfo = $this->ParseOggPageHeader()) { + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; - // First, save what we haven't read yet - $AsYetUnusedData = substr($commentdata, $commentdataoffset); + // First, save what we haven't read yet + $AsYetUnusedData = substr($commentdata, $commentdataoffset); - // Then take that data off the end - $commentdata = substr($commentdata, 0, $commentdataoffset); + // Then take that data off the end + $commentdata = substr($commentdata, 0, $commentdataoffset); - // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct - $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); + // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct + $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); + $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - // Finally, stick the unused data back on the end - $commentdata .= $AsYetUnusedData; + // Finally, stick the unused data back on the end + $commentdata .= $AsYetUnusedData; - //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); - if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) { - $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell()); + //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); + if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) { + $this->warning('undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell()); + break; + } + $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1); + if ($readlength <= 0) { + $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell()); + break; + } + $commentdata .= $this->fread($readlength); + + //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset']; + } else { + $this->warning('failed to ParseOggPageHeader() at offset '.$this->ftell()); break; } - $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1); - if ($readlength <= 0) { - $this->warning('invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell()); - break; - } - $commentdata .= $this->fread($readlength); - - //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset']; } $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset; $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']); @@ -765,6 +825,11 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return true; } + /** + * @param int $mode + * + * @return string|null + */ public static function SpeexBandModeLookup($mode) { static $SpeexBandModeLookup = array(); if (empty($SpeexBandModeLookup)) { @@ -775,8 +840,14 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null); } - + /** + * @param array $OggInfoArray + * @param int $SegmentNumber + * + * @return int + */ public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) { + $segmentlength = 0; for ($i = 0; $i < $SegmentNumber; $i++) { $segmentlength = 0; foreach ($OggInfoArray['segment_table'] as $key => $value) { @@ -789,7 +860,11 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return $segmentlength; } - + /** + * @param int $nominal_bitrate + * + * @return float + */ public static function get_quality_from_nominal_bitrate($nominal_bitrate) { // decrease precision @@ -813,6 +888,11 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return round($qval, 1); // 5 or 4.9 } + /** + * @param int $colorspace_id + * + * @return string|null + */ public static function TheoraColorSpace($colorspace_id) { // http://www.theora.org/doc/Theora.pdf (table 6.3) static $TheoraColorSpaceLookup = array(); @@ -825,6 +905,11 @@ $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 [' return (isset($TheoraColorSpaceLookup[$colorspace_id]) ? $TheoraColorSpaceLookup[$colorspace_id] : null); } + /** + * @param int $pixelformat_id + * + * @return string|null + */ public static function TheoraPixelFormat($pixelformat_id) { // http://www.theora.org/doc/Theora.pdf (table 6.4) static $TheoraPixelFormatLookup = array(); diff --git a/lib/getid3/module.audio.optimfrog.php b/lib/getid3/module.audio.optimfrog.php index 50e0ffd..d84c2c4 100644 --- a/lib/getid3/module.audio.optimfrog.php +++ b/lib/getid3/module.audio.optimfrog.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.optimfrog.php // @@ -14,11 +14,16 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); class getid3_optimfrog extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -44,7 +49,9 @@ class getid3_optimfrog extends getid3_handler return false; } - + /** + * @return bool + */ public function ParseOptimFROGheader42() { // for fileformat of v4.21 and older @@ -72,7 +79,7 @@ class getid3_optimfrog extends getid3_handler $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; $getid3_riff = new getid3_riff($getid3_temp); @@ -91,7 +98,9 @@ class getid3_optimfrog extends getid3_handler return true; } - + /** + * @return bool + */ public function ParseOptimFROGheader45() { // for fileformat of v4.50a and higher @@ -179,6 +188,7 @@ class getid3_optimfrog extends getid3_handler case 'COMP': // unlike other block types, there CAN be multiple COMP blocks + $COMPdata = array(); $COMPdata['offset'] = $BlockOffset; $COMPdata['size'] = $BlockSize; @@ -301,7 +311,7 @@ class getid3_optimfrog extends getid3_handler $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; $getid3_riff = new getid3_riff($getid3_temp); @@ -313,7 +323,11 @@ class getid3_optimfrog extends getid3_handler return true; } - + /** + * @param int $SampleType + * + * @return string|false + */ public static function OptimFROGsampleTypeLookup($SampleType) { static $OptimFROGsampleTypeLookup = array( 0 => 'unsigned int (8-bit)', @@ -331,6 +345,11 @@ class getid3_optimfrog extends getid3_handler return (isset($OptimFROGsampleTypeLookup[$SampleType]) ? $OptimFROGsampleTypeLookup[$SampleType] : false); } + /** + * @param int $SampleType + * + * @return int|false + */ public static function OptimFROGbitsPerSampleTypeLookup($SampleType) { static $OptimFROGbitsPerSampleTypeLookup = array( 0 => 8, @@ -348,6 +367,11 @@ class getid3_optimfrog extends getid3_handler return (isset($OptimFROGbitsPerSampleTypeLookup[$SampleType]) ? $OptimFROGbitsPerSampleTypeLookup[$SampleType] : false); } + /** + * @param int $ChannelConfiguration + * + * @return string|false + */ public static function OptimFROGchannelConfigurationLookup($ChannelConfiguration) { static $OptimFROGchannelConfigurationLookup = array( 0 => 'mono', @@ -356,6 +380,11 @@ class getid3_optimfrog extends getid3_handler return (isset($OptimFROGchannelConfigurationLookup[$ChannelConfiguration]) ? $OptimFROGchannelConfigurationLookup[$ChannelConfiguration] : false); } + /** + * @param int $ChannelConfiguration + * + * @return int|false + */ public static function OptimFROGchannelConfigNumChannelsLookup($ChannelConfiguration) { static $OptimFROGchannelConfigNumChannelsLookup = array( 0 => 1, @@ -365,13 +394,17 @@ class getid3_optimfrog extends getid3_handler } - // static function OptimFROGalgorithmNameLookup($AlgorithID) { // static $OptimFROGalgorithmNameLookup = array(); // return (isset($OptimFROGalgorithmNameLookup[$AlgorithID]) ? $OptimFROGalgorithmNameLookup[$AlgorithID] : false); // } + /** + * @param int $EncoderID + * + * @return string + */ public static function OptimFROGencoderNameLookup($EncoderID) { // version = (encoderID >> 4) + 4500 // system = encoderID & 0xF @@ -387,6 +420,11 @@ class getid3_optimfrog extends getid3_handler return $EncoderVersion.' ('.(isset($OptimFROGencoderSystemLookup[$EncoderSystemID]) ? $OptimFROGencoderSystemLookup[$EncoderSystemID] : 'undefined encoder type (0x'.dechex($EncoderSystemID).')').')'; } + /** + * @param int $CompressionID + * + * @return string + */ public static function OptimFROGcompressionLookup($CompressionID) { // mode = compression >> 3 // speedup = compression & 0x07 @@ -409,6 +447,11 @@ class getid3_optimfrog extends getid3_handler return (isset($OptimFROGencoderModeLookup[$CompressionModeID]) ? $OptimFROGencoderModeLookup[$CompressionModeID] : 'undefined mode (0x'.str_pad(dechex($CompressionModeID), 2, '0', STR_PAD_LEFT).')'); } + /** + * @param int $CompressionID + * + * @return string + */ public static function OptimFROGspeedupLookup($CompressionID) { // mode = compression >> 3 // speedup = compression & 0x07 diff --git a/lib/getid3/module.audio.rkau.php b/lib/getid3/module.audio.rkau.php index d7c2f09..be63547 100644 --- a/lib/getid3/module.audio.rkau.php +++ b/lib/getid3/module.audio.rkau.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.shorten.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_rkau extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -50,7 +55,7 @@ class getid3_rkau extends getid3_handler $this->RKAUqualityLookup($info['rkau']); $info['rkau']['raw']['flags'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 15, 1)); - $info['rkau']['flags']['joint_stereo'] = (bool) (!($info['rkau']['raw']['flags'] & 0x01)); + $info['rkau']['flags']['joint_stereo'] = !($info['rkau']['raw']['flags'] & 0x01); $info['rkau']['flags']['streaming'] = (bool) ($info['rkau']['raw']['flags'] & 0x02); $info['rkau']['flags']['vrq_lossy_mode'] = (bool) ($info['rkau']['raw']['flags'] & 0x04); @@ -70,13 +75,17 @@ class getid3_rkau extends getid3_handler $info['audio']['sample_rate'] = $info['rkau']['sample_rate']; $info['playtime_seconds'] = $info['rkau']['source_bytes'] / ($info['rkau']['sample_rate'] * $info['rkau']['channels'] * ($info['rkau']['bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($info['rkau']['compressed_bytes'] * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = getid3_lib::SafeDiv($info['rkau']['compressed_bytes'] * 8, $info['playtime_seconds']); return true; } - + /** + * @param array $RKAUdata + * + * @return bool + */ public function RKAUqualityLookup(&$RKAUdata) { $level = ($RKAUdata['raw']['quality'] & 0xF0) >> 4; $quality = $RKAUdata['raw']['quality'] & 0x0F; diff --git a/lib/getid3/module.audio.shorten.php b/lib/getid3/module.audio.shorten.php index 8d5c5d4..2e4060c 100644 --- a/lib/getid3/module.audio.shorten.php +++ b/lib/getid3/module.audio.shorten.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.shorten.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_shorten extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -38,7 +43,7 @@ class getid3_shorten extends getid3_handler $this->fseek($info['avdataend'] - 12); $SeekTableSignatureTest = $this->fread(12); - $info['shn']['seektable']['present'] = (bool) (substr($SeekTableSignatureTest, 4, 8) == 'SHNAMPSK'); + $info['shn']['seektable']['present'] = substr($SeekTableSignatureTest, 4, 8) == 'SHNAMPSK'; if ($info['shn']['seektable']['present']) { $info['shn']['seektable']['length'] = getid3_lib::LittleEndian2Int(substr($SeekTableSignatureTest, 0, 4)); $info['shn']['seektable']['offset'] = $info['avdataend'] - $info['shn']['seektable']['length']; @@ -148,6 +153,9 @@ class getid3_shorten extends getid3_handler if (!empty($output) && (substr($output, 12, 4) == 'fmt ')) { + if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; + } getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); $fmt_size = getid3_lib::LittleEndian2Int(substr($output, 16, 4)); @@ -158,7 +166,7 @@ class getid3_shorten extends getid3_handler if (substr($output, 20 + $fmt_size, 4) == 'data') { - $info['playtime_seconds'] = getid3_lib::LittleEndian2Int(substr($output, 20 + 4 + $fmt_size, 4)) / $DecodedWAVFORMATEX['raw']['nAvgBytesPerSec']; + $info['playtime_seconds'] = getid3_lib::SafeDiv(getid3_lib::LittleEndian2Int(substr($output, 20 + 4 + $fmt_size, 4)), $DecodedWAVFORMATEX['raw']['nAvgBytesPerSec']); } else { @@ -167,7 +175,7 @@ class getid3_shorten extends getid3_handler } - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8; + $info['audio']['bitrate'] = getid3_lib::SafeDiv($info['avdataend'] - $info['avdataoffset'], $info['playtime_seconds']) * 8; } else { diff --git a/lib/getid3/module.audio.tak.php b/lib/getid3/module.audio.tak.php new file mode 100644 index 0000000..0391d05 --- /dev/null +++ b/lib/getid3/module.audio.tak.php @@ -0,0 +1,216 @@ + // +// available at http://getid3.sourceforge.net // +// or https://www.getid3.org // +// also https://github.com/JamesHeinrich/getID3 // +///////////////////////////////////////////////////////////////// +// See readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.audio.tak.php // +// module for analyzing Tom's lossless Audio Kompressor // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_tak extends getid3_handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $info['fileformat'] = 'tak'; + $info['audio']['dataformat'] = 'tak'; + $info['audio']['bitrate_mode'] = 'vbr'; + $info['audio']['lossless'] = true; + + $info['tak_audio']['raw'] = array(); + $thisfile_takaudio = &$info['tak_audio']; + $thisfile_takaudio_raw = &$thisfile_takaudio['raw']; + + $this->fseek($info['avdataoffset']); + $TAKMetaData = $this->fread(4); + + $thisfile_takaudio_raw['magic'] = $TAKMetaData; + $magic = 'tBaK'; + if ($thisfile_takaudio_raw['magic'] != $magic) { + $this->error('Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($thisfile_takaudio_raw['magic']).'"'); + unset($info['fileformat']); + return false; + } + $offset = 4; //skip magic + $this->fseek($offset); + $TAKMetaData = $this->fread(4); //read Metadata Block Header + $objtype = getid3_lib::BigEndian2Int(substr($TAKMetaData, 0, 1)); //Metadata Block Object Type + $objlength = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 1, 3)); //Metadata Block Object Lenght excluding header + if ($objtype == 1) { //The First Metadata Block Object must be of Type 1 (STREAMINFO) + $offset += 4; //skip to Metadata Block contents + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); // Get the raw Metadata Block Data + $thisfile_takaudio_raw['STREAMINFO'] = getid3_lib::LittleEndian2Bin(substr($TAKMetaData, 0, $objlength - 3)); + $offset += $objlength; // Move to the next Metadata Block Object + $thisfile_takaudio['channels'] = getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 1, 4)) + 1; + $thisfile_takaudio['bits_per_sample'] = getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 5, 5)) + 8; + $thisfile_takaudio['sample_rate'] = getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 10, 18)) + 6000; + $thisfile_takaudio['samples'] = getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 31, 35)); + $thisfile_takaudio['framesize'] = self::TAKFramesizeLookup(getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 66, 4))); + $thisfile_takaudio['codectype'] = self::TAKCodecTypeLookup(getid3_lib::Bin2Dec(substr($thisfile_takaudio_raw['STREAMINFO'], 74, 6))); + } else { + $this->error('Expecting Type 1 (STREAMINFO) Metadata Object header, but found Type "'.$objtype.'" Object instead'); + unset($info['fileformat']); + return false; + } + $this->fseek($offset); + $TAKMetaData = $this->fread(4); + $objtype = getid3_lib::BigEndian2Int(substr($TAKMetaData, 0, 1)); + $objlength = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 1, 3)); + while ($objtype != 0) { + switch ($objtype) { + case 4 : + // ENCODERINFO Metadata Block + $offset += 4; + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); + $ver = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 0, 3)); + $major = ($ver & 0xff0000) >> 16; + $minor = ($ver & 0x00ff00) >> 8; + $revision= $ver & 0x0000ff; + $thisfile_takaudio['version'] = 'TAK V '.$major.'.'.$minor.'.'.$revision; + $thisfile_takaudio['profile'] = self::TAKProfileLookup(getid3_lib::BigEndian2Int(substr($TAKMetaData, 3, 1))); + $offset += $objlength; + break; + case 6 : + // MD5 Checksum Metadata Block + $offset += 4; + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); + $thisfile_takaudio_raw['MD5Data'] = substr($TAKMetaData, 0, 16); + $offset += $objlength; + break; + case 7 : + // LASTFRAME Metadata Block + $offset += 4; + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); + $thisfile_takaudio['lastframe_pos'] = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 0, 5)); + $thisfile_takaudio['last_frame_size'] = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 5, 3)); + $offset += $objlength; + break; + case 3 : + // ORIGINALFILEDATA Metadata Block + $offset += 4; + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); + $headersize = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 0, 3)); + $footersize = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 3, 3)); + if ($headersize) $thisfile_takaudio_raw['header_data'] = substr($TAKMetaData, 6, $headersize); + if ($footersize) $thisfile_takaudio_raw['footer_data'] = substr($TAKMetaData, $headersize, $footersize); + $offset += $objlength; + break; + default : + // PADDING or SEEKTABLE Metadata Block. Just skip it + $offset += 4; + $this->fseek($offset); + $TAKMetaData = $this->fread($objlength); + $offset += $objlength; + break; + } + $this->fseek($offset); + $TAKMetaData = $this->fread(4); + $objtype = getid3_lib::BigEndian2Int(substr($TAKMetaData, 0, 1)); + $objlength = getid3_lib::LittleEndian2Int(substr($TAKMetaData, 1, 3)); + } + // Finished all Metadata Blocks. So update $info['avdataoffset'] because next block is the first Audio data block + $info['avdataoffset'] = $offset; + + $info['audio']['channels'] = $thisfile_takaudio['channels']; + if ($thisfile_takaudio['sample_rate'] == 0) { + $this->error('Corrupt TAK file: samplerate == zero'); + return false; + } + $info['audio']['sample_rate'] = $thisfile_takaudio['sample_rate']; + $thisfile_takaudio['playtime'] = $thisfile_takaudio['samples'] / $thisfile_takaudio['sample_rate']; + if ($thisfile_takaudio['playtime'] == 0) { + $this->error('Corrupt TAK file: playtime == zero'); + return false; + } + $info['playtime_seconds'] = $thisfile_takaudio['playtime']; + $thisfile_takaudio['compressed_size'] = $info['avdataend'] - $info['avdataoffset']; + $thisfile_takaudio['uncompressed_size'] = $thisfile_takaudio['samples'] * $thisfile_takaudio['channels'] * ($thisfile_takaudio['bits_per_sample'] / 8); + if ($thisfile_takaudio['uncompressed_size'] == 0) { + $this->error('Corrupt TAK file: uncompressed_size == zero'); + return false; + } + $thisfile_takaudio['compression_ratio'] = $thisfile_takaudio['compressed_size'] / ($thisfile_takaudio['uncompressed_size'] + $offset); + $thisfile_takaudio['bitrate'] = (($thisfile_takaudio['samples'] * $thisfile_takaudio['channels'] * $thisfile_takaudio['bits_per_sample']) / $thisfile_takaudio['playtime']) * $thisfile_takaudio['compression_ratio']; + $info['audio']['bitrate'] = $thisfile_takaudio['bitrate']; + + if (empty($thisfile_takaudio_raw['MD5Data'])) { + //$this->warning('MD5Data is not set'); + } elseif ($thisfile_takaudio_raw['MD5Data'] === str_repeat("\x00", 16)) { + //$this->warning('MD5Data is null'); + } else { + $info['md5_data_source'] = ''; + $md5 = $thisfile_takaudio_raw['MD5Data']; + for ($i = 0; $i < strlen($md5); $i++) { + $info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT); + } + if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) { + unset($info['md5_data_source']); + } + } + + foreach (array('bits_per_sample', 'version', 'profile') as $key) { + if (!empty($thisfile_takaudio[$key])) { + $info['audio'][$key] = $thisfile_takaudio[$key]; + } + } + + return true; + } + + public function TAKFramesizeLookup($framesize) { + static $TAKFramesizeLookup = array( + 0 => '94 ms', + 1 => '125 ms', + 2 => '188 ms', + 3 => '250 ms', + 4 => '4096 samples', + 5 => '8192 samples', + 6 => '16384 samples', + 7 => '512 samples', + 8 => '1024 samples', + 9 => '2048 samples' + ); + return (isset($TAKFramesizeLookup[$framesize]) ? $TAKFramesizeLookup[$framesize] : 'invalid'); + } + public function TAKCodecTypeLookup($code) { + static $TAKCodecTypeLookup = array( + 0 => 'Integer 24 bit (TAK 1.0)', + 1 => 'Experimental!', + 2 => 'Integer 24 bit (TAK 2.0)', + 3 => 'LossyWav (TAK 2.1)', + 4 => 'Integer 24 bit MC (TAK 2.2)' + ); + return (isset($TAKCodecTypeLookup[$code]) ? $TAKCodecTypeLookup[$code] : 'invalid'); + } + public function TAKProfileLookup($code) { + $out ='-p'; + $evaluation = ($code & 0xf0) >> 4; + $compresion = $code & 0x0f; + static $TAKEvaluationLookup = array( + 0 => '', + 1 => 'e', + 2 => 'm' + ); + return (isset($TAKEvaluationLookup[$evaluation]) ? $out .= $compresion . $TAKEvaluationLookup[$evaluation] : 'invalid'); + } + +} diff --git a/lib/getid3/module.audio.tta.php b/lib/getid3/module.audio.tta.php index 78d27b0..0f0aa8b 100644 --- a/lib/getid3/module.audio.tta.php +++ b/lib/getid3/module.audio.tta.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.tta.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_tta extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -39,7 +44,7 @@ class getid3_tta extends getid3_handler return false; } - switch ($ttaheader{3}) { + switch ($ttaheader[3]) { case "\x01": // TTA v1.x case "\x02": // TTA v1.x case "\x03": // TTA v1.x @@ -47,14 +52,14 @@ class getid3_tta extends getid3_handler $info['tta']['major_version'] = 1; $info['avdataoffset'] += 16; - $info['tta']['compression_level'] = ord($ttaheader{3}); + $info['tta']['compression_level'] = ord($ttaheader[3]); $info['tta']['channels'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 4, 2)); $info['tta']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 6, 2)); $info['tta']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 8, 4)); $info['tta']['samples_per_channel'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 12, 4)); $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['samples_per_channel'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['tta']['samples_per_channel'], $info['tta']['sample_rate']); break; case '2': // TTA v2.x @@ -70,7 +75,7 @@ class getid3_tta extends getid3_handler $info['tta']['data_length'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 16, 4)); $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['tta']['data_length'], $info['tta']['sample_rate']); break; case '1': // TTA v3.x @@ -86,20 +91,19 @@ class getid3_tta extends getid3_handler $info['tta']['crc32_footer'] = substr($ttaheader, 18, 4); $info['tta']['seek_point'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 22, 4)); - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['tta']['data_length'], $info['tta']['sample_rate']); break; default: - $this->error('This version of getID3() ['.$this->getid3->version().'] only knows how to handle TTA v1 and v2 - it may not work correctly with this file which appears to be TTA v'.$ttaheader{3}); + $this->error('This version of getID3() ['.$this->getid3->version().'] only knows how to handle TTA v1 and v2 - it may not work correctly with this file which appears to be TTA v'.$ttaheader[3]); return false; - break; } $info['audio']['encoder'] = 'TTA v'.$info['tta']['major_version']; $info['audio']['bits_per_sample'] = $info['tta']['bits_per_sample']; $info['audio']['sample_rate'] = $info['tta']['sample_rate']; $info['audio']['channels'] = $info['tta']['channels']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); return true; } diff --git a/lib/getid3/module.audio.voc.php b/lib/getid3/module.audio.voc.php index 3803d8a..9e771b1 100644 --- a/lib/getid3/module.audio.voc.php +++ b/lib/getid3/module.audio.voc.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.voc.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_voc extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -54,15 +59,17 @@ class getid3_voc extends getid3_handler $thisfile_voc['header']['datablock_offset'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 20, 2)); $thisfile_voc['header']['minor_version'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 22, 1)); $thisfile_voc['header']['major_version'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 23, 1)); + $thisfile_voc['blocktypes'] = array(); do { $BlockOffset = $this->ftell(); $BlockData = $this->fread(4); - $BlockType = ord($BlockData{0}); + $BlockType = ord($BlockData[0]); $BlockSize = getid3_lib::LittleEndian2Int(substr($BlockData, 1, 3)); $ThisBlock = array(); + /** @phpstan-ignore-next-line */ getid3_lib::safe_inc($thisfile_voc['blocktypes'][$BlockType], 1); switch ($BlockType) { case 0: // Terminator @@ -133,7 +140,7 @@ class getid3_voc extends getid3_handler $thisfile_audio['sample_rate'] = $ThisBlock['sample_rate']; $thisfile_audio['bits_per_sample'] = $ThisBlock['bits_per_sample']; - $thisfile_audio['channels'] = $ThisBlock['channels']; + $thisfile_audio['channels'] = $ThisBlock['channels'] ?: 1; break; default: @@ -157,13 +164,18 @@ class getid3_voc extends getid3_handler ksort($thisfile_voc['blocktypes']); if (!empty($thisfile_voc['compressed_bits_per_sample'])) { - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($thisfile_voc['compressed_bits_per_sample'] * $thisfile_audio['channels'] * $thisfile_audio['sample_rate']); - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['playtime_seconds'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $thisfile_voc['compressed_bits_per_sample'] * $thisfile_audio['channels'] * $thisfile_audio['sample_rate']); + $thisfile_audio['bitrate'] = getid3_lib::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); } return true; } + /** + * @param int $index + * + * @return string + */ public function VOCcompressionTypeLookup($index) { static $VOCcompressionTypeLookup = array( 0 => '8-bit', @@ -174,6 +186,11 @@ class getid3_voc extends getid3_handler return (isset($VOCcompressionTypeLookup[$index]) ? $VOCcompressionTypeLookup[$index] : 'Multi DAC ('.($index - 3).') channels'); } + /** + * @param int $index + * + * @return string|false + */ public function VOCwFormatLookup($index) { static $VOCwFormatLookup = array( 0x0000 => '8-bit unsigned PCM', @@ -188,6 +205,11 @@ class getid3_voc extends getid3_handler return (isset($VOCwFormatLookup[$index]) ? $VOCwFormatLookup[$index] : false); } + /** + * @param int $index + * + * @return int|false + */ public function VOCwFormatActualBitsPerSampleLookup($index) { static $VOCwFormatLookup = array( 0x0000 => 8, diff --git a/lib/getid3/module.audio.vqf.php b/lib/getid3/module.audio.vqf.php index a6a391c..55c7881 100644 --- a/lib/getid3/module.audio.vqf.php +++ b/lib/getid3/module.audio.vqf.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.vqf.php // @@ -14,9 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_vqf extends getid3_handler { + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -136,6 +142,11 @@ class getid3_vqf extends getid3_handler return true; } + /** + * @param int $frequencyid + * + * @return int + */ public function VQFchannelFrequencyLookup($frequencyid) { static $VQFchannelFrequencyLookup = array( 11 => 11025, @@ -145,6 +156,11 @@ class getid3_vqf extends getid3_handler return (isset($VQFchannelFrequencyLookup[$frequencyid]) ? $VQFchannelFrequencyLookup[$frequencyid] : $frequencyid * 1000); } + /** + * @param string $shortname + * + * @return string + */ public function VQFcommentNiceNameLookup($shortname) { static $VQFcommentNiceNameLookup = array( 'NAME' => 'title', diff --git a/lib/getid3/module.audio.wavpack.php b/lib/getid3/module.audio.wavpack.php index b54c179..53602c3 100644 --- a/lib/getid3/module.audio.wavpack.php +++ b/lib/getid3/module.audio.wavpack.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.audio.wavpack.php // @@ -14,22 +14,35 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_wavpack extends getid3_handler { + /** + * Avoid scanning all frames (break after finding ID_RIFF_HEADER and ID_CONFIG_BLOCK, + * significantly faster for very large files but other data may be missed + * + * @var bool + */ + public $quick_parsing = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; $this->fseek($info['avdataoffset']); + $found_blocks = array(); while (true) { $wavpackheader = $this->fread(32); if ($this->ftell() >= $info['avdataend']) { break; - } elseif (feof($this->getid3->fp)) { + } elseif ($this->feof()) { break; } elseif ( isset($info['wavpack']['blockheader']['total_samples']) && @@ -100,8 +113,8 @@ class getid3_wavpack extends getid3_handler return false; } - $info['wavpack']['blockheader']['minor_version'] = ord($wavpackheader{8}); - $info['wavpack']['blockheader']['major_version'] = ord($wavpackheader{9}); + $info['wavpack']['blockheader']['minor_version'] = ord($wavpackheader[8]); + $info['wavpack']['blockheader']['major_version'] = ord($wavpackheader[9]); if (($info['wavpack']['blockheader']['major_version'] != 4) || (($info['wavpack']['blockheader']['minor_version'] < 4) && @@ -120,8 +133,8 @@ class getid3_wavpack extends getid3_handler return false; } - $info['wavpack']['blockheader']['track_number'] = ord($wavpackheader{10}); // unused - $info['wavpack']['blockheader']['index_number'] = ord($wavpackheader{11}); // unused + $info['wavpack']['blockheader']['track_number'] = ord($wavpackheader[10]); // unused + $info['wavpack']['blockheader']['index_number'] = ord($wavpackheader[11]); // unused $info['wavpack']['blockheader']['total_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 12, 4)); $info['wavpack']['blockheader']['block_index'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 16, 4)); $info['wavpack']['blockheader']['block_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 20, 4)); @@ -144,16 +157,17 @@ class getid3_wavpack extends getid3_handler $info['audio']['lossless'] = !$info['wavpack']['blockheader']['flags']['hybrid']; } - while (!feof($this->getid3->fp) && ($this->ftell() < ($blockheader_offset + $blockheader_size + 8))) { + while (!$this->feof() && ($this->ftell() < ($blockheader_offset + $blockheader_size + 8))) { $metablock = array('offset'=>$this->ftell()); $metablockheader = $this->fread(2); - if (feof($this->getid3->fp)) { + if ($this->feof()) { break; } - $metablock['id'] = ord($metablockheader{0}); + $metablock['id'] = ord($metablockheader[0]); $metablock['function_id'] = ($metablock['id'] & 0x3F); $metablock['function_name'] = $this->WavPackMetablockNameLookup($metablock['function_id']); + $found_blocks[$metablock['function_name']] = (isset($found_blocks[$metablock['function_name']]) ? $found_blocks[$metablock['function_name']] : 0) + 1; // cannot use getid3_lib::safe_inc without warnings(?) // The 0x20 bit in the id of the meta subblocks (which is defined as // ID_OPTIONAL_DATA) is a permanent part of the id. The idea is that @@ -171,6 +185,7 @@ class getid3_wavpack extends getid3_handler } $metablock['size'] = getid3_lib::LittleEndian2Int(substr($metablockheader, 1)) * 2; // size is stored in words $metablock['data'] = null; + $metablock['comments'] = array(); if ($metablock['size'] > 0) { @@ -219,7 +234,7 @@ class getid3_wavpack extends getid3_handler $original_wav_filesize = getid3_lib::LittleEndian2Int(substr($metablock['data'], 4, 4)); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_riff = new getid3_riff($getid3_temp); $getid3_riff->ParseRIFFdata($metablock['data']); $metablock['riff'] = $getid3_temp->info['riff']; @@ -228,20 +243,24 @@ class getid3_wavpack extends getid3_handler $metablock['riff']['original_filesize'] = $original_wav_filesize; $info['wavpack']['riff_trailer_size'] = $original_wav_filesize - $metablock['riff']['WAVE']['data'][0]['size'] - $metablock['riff']['header_size']; - $info['playtime_seconds'] = $info['wavpack']['blockheader']['total_samples'] / $info['audio']['sample_rate']; + $info['playtime_seconds'] = getid3_lib::SafeDiv($info['wavpack']['blockheader']['total_samples'], $info['audio']['sample_rate']); // Safe RIFF header in case there's a RIFF footer later $metablockRIFFheader = $metablock['data']; + if ($this->quick_parsing && !empty($found_blocks['RIFF header']) && !empty($found_blocks['Config Block'])) { + $this->warning('WavPack quick-parsing enabled (faster at parsing large fules, but may miss some data)'); + break 2; + } break; case 0x22: // ID_RIFF_TRAILER - $metablockRIFFfooter = $metablockRIFFheader.$metablock['data']; + $metablockRIFFfooter = isset($metablockRIFFheader) ? $metablockRIFFheader : ''.$metablock['data']; getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); $startoffset = $metablock['offset'] + ($metablock['large_block'] ? 4 : 2); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataend'] = $info['avdataend']; //$getid3_temp->info['fileformat'] = 'riff'; $getid3_riff = new getid3_riff($getid3_temp); @@ -312,6 +331,10 @@ class getid3_wavpack extends getid3_handler } elseif (isset($info['audio']['encoder_options'])) { unset($info['audio']['encoder_options']); } + if ($this->quick_parsing && !empty($found_blocks['RIFF header']) && !empty($found_blocks['Config Block'])) { + $this->warning('WavPack quick-parsing enabled (faster at parsing large fules, but may miss some data)'); + break 2; + } break; @@ -364,11 +387,14 @@ class getid3_wavpack extends getid3_handler $info['audio']['dataformat'] = 'wvc'; } - return true; } - + /** + * @param int $id + * + * @return string + */ public function WavPackMetablockNameLookup(&$id) { static $WavPackMetablockNameLookup = array( 0x00 => 'Dummy', diff --git a/lib/getid3/module.graphic.bmp.php b/lib/getid3/module.graphic.bmp.php index 90cbb38..fb39472 100644 --- a/lib/getid3/module.graphic.bmp.php +++ b/lib/getid3/module.graphic.bmp.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.bmp.php // @@ -14,12 +14,29 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_bmp extends getid3_handler { + /** + * return BMP palette + * + * @var bool + */ public $ExtractPalette = false; + + /** + * return image data + * + * @var bool + */ public $ExtractData = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -319,313 +336,322 @@ class getid3_bmp extends getid3_handler } if ($this->ExtractData) { - $this->fseek($thisfile_bmp_header_raw['data_offset']); - $RowByteLength = ceil(($thisfile_bmp_header_raw['width'] * ($thisfile_bmp_header_raw['bits_per_pixel'] / 8)) / 4) * 4; // round up to nearest DWORD boundry - $BMPpixelData = $this->fread($thisfile_bmp_header_raw['height'] * $RowByteLength); - $pixeldataoffset = 0; - $thisfile_bmp_header_raw['compression'] = (isset($thisfile_bmp_header_raw['compression']) ? $thisfile_bmp_header_raw['compression'] : ''); - switch ($thisfile_bmp_header_raw['compression']) { + if (!$thisfile_bmp_header_raw['width'] || !$thisfile_bmp_header_raw['height']) { + $thisfile_bmp['data'] = array(); + } else { + $this->fseek($thisfile_bmp_header_raw['data_offset']); + $RowByteLength = ceil(($thisfile_bmp_header_raw['width'] * ($thisfile_bmp_header_raw['bits_per_pixel'] / 8)) / 4) * 4; // round up to nearest DWORD boundry + $BMPpixelData = $this->fread($thisfile_bmp_header_raw['height'] * $RowByteLength); + $pixeldataoffset = 0; + $thisfile_bmp_header_raw['compression'] = (isset($thisfile_bmp_header_raw['compression']) ? $thisfile_bmp_header_raw['compression'] : ''); + switch ($thisfile_bmp_header_raw['compression']) { - case 0: // BI_RGB - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 1: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData{$pixeldataoffset++}); - for ($i = 7; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x01 << $i)) >> $i; + case 0: // BI_RGB + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 1: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { + $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); + for ($i = 7; $i >= 0; $i--) { + $paletteindex = ($paletteindexbyte & (0x01 << $i)) >> $i; + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $col++; + } + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; + } + } + break; + + case 4: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { + $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); + for ($i = 1; $i >= 0; $i--) { + $paletteindex = ($paletteindexbyte & (0x0F << (4 * $i))) >> (4 * $i); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $col++; + } + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; + } + } + break; + + case 8: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $paletteindex = ord($BMPpixelData[$pixeldataoffset++]); $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; + break; - case 4: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData{$pixeldataoffset++}); - for ($i = 1; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x0F << (4 * $i))) >> (4 * $i); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; + case 24: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); + $pixeldataoffset += 3; + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; + break; - case 8: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $paletteindex = ord($BMPpixelData{$pixeldataoffset++}); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 24: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData{$pixeldataoffset+2}) << 16) | (ord($BMPpixelData{$pixeldataoffset+1}) << 8) | ord($BMPpixelData{$pixeldataoffset}); - $pixeldataoffset += 3; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 32: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData{$pixeldataoffset+3}) << 24) | (ord($BMPpixelData{$pixeldataoffset+2}) << 16) | (ord($BMPpixelData{$pixeldataoffset+1}) << 8) | ord($BMPpixelData{$pixeldataoffset}); - $pixeldataoffset += 4; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 16: - // ? - break; - - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; - - - case 1: // BI_RLE8 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 8: - $pixelcounter = 0; - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { - - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; - - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; - - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; - - default: - // In absolute mode, the first byte is zero and the second byte is a - // value in the range 03H through FFH. The second byte represents the - // number of bytes that follow, each of which contains the color index - // of a single pixel. Each run must be aligned on a word boundary. - for ($i = 0; $i < $secondbyte; $i++) { - $paletteindex = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } - break; + case 32: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+3]) << 24) | (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); + $pixeldataoffset += 4; } - - } else { - - // encoded mode - the first byte specifies the number of consecutive pixels - // to be drawn using the color index contained in the second byte. - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$secondbyte]; - $pixelcounter++; + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } - } - } - break; + break; - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; + case 16: + // ? + break; + + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; + case 1: // BI_RLE8 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 8: + $pixelcounter = 0; + while ($pixeldataoffset < strlen($BMPpixelData)) { + $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + if ($firstbyte == 0) { - case 2: // BI_RLE4 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 4: - $pixelcounter = 0; - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { + // escaped/absolute mode - the first byte of the pair can be set to zero to + // indicate an escape character that denotes the end of a line, the end of + // a bitmap, or a delta, depending on the value of the second byte. + switch ($secondbyte) { + case 0: + // end of line + // no need for special processing, just ignore + break; - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; + case 1: + // end of bitmap + $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case + break; - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; + case 2: + // delta - The 2 bytes following the escape contain unsigned values + // indicating the horizontal and vertical offsets of the next pixel + // from the current position. + $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; + $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; + $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; + break; - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; + default: + // In absolute mode, the first byte is zero and the second byte is a + // value in the range 03H through FFH. The second byte represents the + // number of bytes that follow, each of which contains the color index + // of a single pixel. Each run must be aligned on a word boundary. + for ($i = 0; $i < $secondbyte; $i++) { + $paletteindex = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $pixelcounter++; + } + while (($pixeldataoffset % 2) != 0) { + // Each run must be aligned on a word boundary. + $pixeldataoffset++; + } + break; + } - default: - // In absolute mode, the first byte is zero. The second byte contains the number - // of color indexes that follow. Subsequent bytes contain color indexes in their - // high- and low-order 4 bits, one color index for each pixel. In absolute mode, - // each run must be aligned on a word boundary. - unset($paletteindexes); - for ($i = 0; $i < ceil($secondbyte / 2); $i++) { - $paletteindexbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $paletteindexes[] = ($paletteindexbyte & 0xF0) >> 4; - $paletteindexes[] = ($paletteindexbyte & 0x0F); - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } + } else { + + // encoded mode - the first byte specifies the number of consecutive pixels + // to be drawn using the color index contained in the second byte. + for ($i = 0; $i < $firstbyte; $i++) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$secondbyte]; + $pixelcounter++; + } - foreach ($paletteindexes as $paletteindex) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - break; } + } + break; - } else { + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; + + + + case 2: // BI_RLE4 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 4: + $pixelcounter = 0; + $paletteindexes = array(); + while ($pixeldataoffset < strlen($BMPpixelData)) { + $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + if ($firstbyte == 0) { + + // escaped/absolute mode - the first byte of the pair can be set to zero to + // indicate an escape character that denotes the end of a line, the end of + // a bitmap, or a delta, depending on the value of the second byte. + switch ($secondbyte) { + case 0: + // end of line + // no need for special processing, just ignore + break; + + case 1: + // end of bitmap + $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case + break; + + case 2: + // delta - The 2 bytes following the escape contain unsigned values + // indicating the horizontal and vertical offsets of the next pixel + // from the current position. + $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; + $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; + $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; + break; + + default: + // In absolute mode, the first byte is zero. The second byte contains the number + // of color indexes that follow. Subsequent bytes contain color indexes in their + // high- and low-order 4 bits, one color index for each pixel. In absolute mode, + // each run must be aligned on a word boundary. + $paletteindexes = array(); + for ($i = 0; $i < ceil($secondbyte / 2); $i++) { + $paletteindexbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $paletteindexes[] = ($paletteindexbyte & 0xF0) >> 4; + $paletteindexes[] = ($paletteindexbyte & 0x0F); + } + while (($pixeldataoffset % 2) != 0) { + // Each run must be aligned on a word boundary. + $pixeldataoffset++; + } + + foreach ($paletteindexes as $paletteindex) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $pixelcounter++; + } + break; + } + + } else { + + // encoded mode - the first byte of the pair contains the number of pixels to be + // drawn using the color indexes in the second byte. The second byte contains two + // color indexes, one in its high-order 4 bits and one in its low-order 4 bits. + // The first of the pixels is drawn using the color specified by the high-order + // 4 bits, the second is drawn using the color in the low-order 4 bits, the third + // is drawn using the color in the high-order 4 bits, and so on, until all the + // pixels specified by the first byte have been drawn. + $paletteindexes[0] = ($secondbyte & 0xF0) >> 4; + $paletteindexes[1] = ($secondbyte & 0x0F); + for ($i = 0; $i < $firstbyte; $i++) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindexes[($i % 2)]]; + $pixelcounter++; + } - // encoded mode - the first byte of the pair contains the number of pixels to be - // drawn using the color indexes in the second byte. The second byte contains two - // color indexes, one in its high-order 4 bits and one in its low-order 4 bits. - // The first of the pixels is drawn using the color specified by the high-order - // 4 bits, the second is drawn using the color in the low-order 4 bits, the third - // is drawn using the color in the high-order 4 bits, and so on, until all the - // pixels specified by the first byte have been drawn. - $paletteindexes[0] = ($secondbyte & 0xF0) >> 4; - $paletteindexes[1] = ($secondbyte & 0x0F); - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindexes[($i % 2)]]; - $pixelcounter++; } - } - } - break; + break; - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; - case 3: // BI_BITFIELDS - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 16: - case 32: - $redshift = 0; - $greenshift = 0; - $blueshift = 0; - while ((($thisfile_bmp_header_raw['red_mask'] >> $redshift) & 0x01) == 0) { - $redshift++; - } - while ((($thisfile_bmp_header_raw['green_mask'] >> $greenshift) & 0x01) == 0) { - $greenshift++; - } - while ((($thisfile_bmp_header_raw['blue_mask'] >> $blueshift) & 0x01) == 0) { - $blueshift++; - } - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $pixelvalue = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset, $thisfile_bmp_header_raw['bits_per_pixel'] / 8)); - $pixeldataoffset += $thisfile_bmp_header_raw['bits_per_pixel'] / 8; - - $red = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['red_mask']) >> $redshift) / ($thisfile_bmp_header_raw['red_mask'] >> $redshift)) * 255)); - $green = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['green_mask']) >> $greenshift) / ($thisfile_bmp_header_raw['green_mask'] >> $greenshift)) * 255)); - $blue = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['blue_mask']) >> $blueshift) / ($thisfile_bmp_header_raw['blue_mask'] >> $blueshift)) * 255)); - $thisfile_bmp['data'][$row][$col] = (($red << 16) | ($green << 8) | ($blue)); + case 3: // BI_BITFIELDS + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 16: + case 32: + $redshift = 0; + $greenshift = 0; + $blueshift = 0; + while ((($thisfile_bmp_header_raw['red_mask'] >> $redshift) & 0x01) == 0) { + $redshift++; } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; + while ((($thisfile_bmp_header_raw['green_mask'] >> $greenshift) & 0x01) == 0) { + $greenshift++; } - } - break; + while ((($thisfile_bmp_header_raw['blue_mask'] >> $blueshift) & 0x01) == 0) { + $blueshift++; + } + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $pixelvalue = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset, $thisfile_bmp_header_raw['bits_per_pixel'] / 8)); + $pixeldataoffset += $thisfile_bmp_header_raw['bits_per_pixel'] / 8; - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; + $red = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['red_mask']) >> $redshift) / ($thisfile_bmp_header_raw['red_mask'] >> $redshift)) * 255)); + $green = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['green_mask']) >> $greenshift) / ($thisfile_bmp_header_raw['green_mask'] >> $greenshift)) * 255)); + $blue = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['blue_mask']) >> $blueshift) / ($thisfile_bmp_header_raw['blue_mask'] >> $blueshift)) * 255)); + $thisfile_bmp['data'][$row][$col] = (($red << 16) | ($green << 8) | ($blue)); + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; + } + } + break; + + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; - default: // unhandled compression type - $this->error('Unknown/unhandled compression type value ('.$thisfile_bmp_header_raw['compression'].') - cannot decompress pixel data'); - break; + default: // unhandled compression type + $this->error('Unknown/unhandled compression type value ('.$thisfile_bmp_header_raw['compression'].') - cannot decompress pixel data'); + break; + } } } return true; } - + /** + * @param array $BMPinfo + * + * @return bool + */ public function PlotBMP(&$BMPinfo) { $starttime = time(); if (!isset($BMPinfo['bmp']['data']) || !is_array($BMPinfo['bmp']['data'])) { @@ -633,15 +659,15 @@ class getid3_bmp extends getid3_handler return false; } set_time_limit(intval(round($BMPinfo['resolution_x'] * $BMPinfo['resolution_y'] / 10000))); - if ($im = ImageCreateTrueColor($BMPinfo['resolution_x'], $BMPinfo['resolution_y'])) { + if ($im = imagecreatetruecolor($BMPinfo['resolution_x'], $BMPinfo['resolution_y'])) { for ($row = 0; $row < $BMPinfo['resolution_y']; $row++) { for ($col = 0; $col < $BMPinfo['resolution_x']; $col++) { if (isset($BMPinfo['bmp']['data'][$row][$col])) { $red = ($BMPinfo['bmp']['data'][$row][$col] & 0x00FF0000) >> 16; $green = ($BMPinfo['bmp']['data'][$row][$col] & 0x0000FF00) >> 8; $blue = ($BMPinfo['bmp']['data'][$row][$col] & 0x000000FF); - $pixelcolor = ImageColorAllocate($im, $red, $green, $blue); - ImageSetPixel($im, $col, $row, $pixelcolor); + $pixelcolor = imagecolorallocate($im, $red, $green, $blue); + imagesetpixel($im, $col, $row, $pixelcolor); } else { //echo 'ERROR: no data for pixel '.$row.' x '.$col.'
'; //return false; @@ -650,18 +676,23 @@ class getid3_bmp extends getid3_handler } if (headers_sent()) { echo 'plotted '.($BMPinfo['resolution_x'] * $BMPinfo['resolution_y']).' pixels in '.(time() - $starttime).' seconds
'; - ImageDestroy($im); + imagedestroy($im); exit; } else { header('Content-type: image/png'); - ImagePNG($im); - ImageDestroy($im); + imagepng($im); + imagedestroy($im); return true; } } return false; } + /** + * @param int $compressionid + * + * @return string + */ public function BMPcompressionWindowsLookup($compressionid) { static $BMPcompressionWindowsLookup = array( 0 => 'BI_RGB', @@ -674,6 +705,11 @@ class getid3_bmp extends getid3_handler return (isset($BMPcompressionWindowsLookup[$compressionid]) ? $BMPcompressionWindowsLookup[$compressionid] : 'invalid'); } + /** + * @param int $compressionid + * + * @return string + */ public function BMPcompressionOS2Lookup($compressionid) { static $BMPcompressionOS2Lookup = array( 0 => 'BI_RGB', diff --git a/lib/getid3/module.graphic.efax.php b/lib/getid3/module.graphic.efax.php index 8c62854..82ea354 100644 --- a/lib/getid3/module.graphic.efax.php +++ b/lib/getid3/module.graphic.efax.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.efax.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_efax extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -42,10 +47,8 @@ class getid3_efax extends getid3_handler $info['efax']['header']['pages'] = getid3_lib::LittleEndian2Int(substr($efaxheader, 198, 2)); $info['efax']['header']['data_bytes'] = getid3_lib::LittleEndian2Int(substr($efaxheader, 202, 4)); -$this->error('eFax parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; - - return true; + $this->error('eFax parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; } } diff --git a/lib/getid3/module.graphic.gif.php b/lib/getid3/module.graphic.gif.php index c73c0ff..f971913 100644 --- a/lib/getid3/module.graphic.gif.php +++ b/lib/getid3/module.graphic.gif.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.gif.php // @@ -14,12 +14,21 @@ // /// ///////////////////////////////////////////////////////////////// -// https://www.w3.org/Graphics/GIF/spec-gif89a.txt -// http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp +/** + * @link https://www.w3.org/Graphics/GIF/spec-gif89a.txt + * @link http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + * @link http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension + */ + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_gif extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -43,6 +52,10 @@ class getid3_gif extends getid3_handler return false; } + //if (!$this->getid3->option_extra_info) { + // $this->warning('GIF Extensions and Global Color Table not returned due to !getid3->option_extra_info'); + //} + $info['gif']['header']['raw']['version'] = substr($GIFheader, $offset, 3); $offset += 3; $info['gif']['header']['raw']['width'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 2)); @@ -82,16 +95,20 @@ class getid3_gif extends getid3_handler if ($info['gif']['header']['flags']['global_color_table']) { $GIFcolorTable = $this->fread(3 * $info['gif']['header']['global_color_size']); - $offset = 0; - for ($i = 0; $i < $info['gif']['header']['global_color_size']; $i++) { - $red = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); - $green = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); - $blue = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); - $info['gif']['global_color_table'][$i] = (($red << 16) | ($green << 8) | ($blue)); + if ($this->getid3->option_extra_info) { + $offset = 0; + for ($i = 0; $i < $info['gif']['header']['global_color_size']; $i++) { + $red = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); + $green = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); + $blue = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); + $info['gif']['global_color_table'][$i] = (($red << 16) | ($green << 8) | ($blue)); + $info['gif']['global_color_table_rgb'][$i] = sprintf('%02X%02X%02X', $red, $green, $blue); + } } } // Image Descriptor + $info['gif']['animation']['animated'] = false; while (!feof($this->getid3->fp)) { $NextBlockTest = $this->fread(1); switch ($NextBlockTest) { @@ -152,17 +169,36 @@ class getid3_gif extends getid3_handler $ExtensionBlock['byte_length'] = getid3_lib::LittleEndian2Int(substr($ExtensionBlockData, 2, 1)); $ExtensionBlock['data'] = (($ExtensionBlock['byte_length'] > 0) ? $this->fread($ExtensionBlock['byte_length']) : null); - if (substr($ExtensionBlock['data'], 0, 11) == 'NETSCAPE2.0') { // Netscape Application Block (NAB) - $ExtensionBlock['data'] .= $this->fread(4); - if (substr($ExtensionBlock['data'], 11, 2) == "\x03\x01") { - $info['gif']['animation']['animated'] = true; - $info['gif']['animation']['loop_count'] = getid3_lib::LittleEndian2Int(substr($ExtensionBlock['data'], 13, 2)); - } else { - $this->warning('Expecting 03 01 at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes(substr($ExtensionBlock['data'], 11, 2)).'"'); - } + switch ($ExtensionBlock['function_code']) { + case 0xFF: + // Application Extension + if ($ExtensionBlock['byte_length'] != 11) { + $this->warning('Expected block size of the Application Extension is 11 bytes, found '.$ExtensionBlock['byte_length'].' at offset '.$this->ftell()); + break; + } + + if (substr($ExtensionBlock['data'], 0, 11) !== 'NETSCAPE2.0' + && substr($ExtensionBlock['data'], 0, 11) !== 'ANIMEXTS1.0' + ) { + $this->warning('Ignoring unsupported Application Extension '.substr($ExtensionBlock['data'], 0, 11)); + break; + } + + // Netscape Application Block (NAB) + $ExtensionBlock['data'] .= $this->fread(4); + if (substr($ExtensionBlock['data'], 11, 2) == "\x03\x01") { + $info['gif']['animation']['animated'] = true; + $info['gif']['animation']['loop_count'] = getid3_lib::LittleEndian2Int(substr($ExtensionBlock['data'], 13, 2)); + } else { + $this->warning('Expecting 03 01 at offset '.($this->ftell() - 4).', found "'.getid3_lib::PrintHexBytes(substr($ExtensionBlock['data'], 11, 2)).'"'); + } + + break; } - $info['gif']['extension_blocks'][] = $ExtensionBlock; + if ($this->getid3->option_extra_info) { + $info['gif']['extension_blocks'][] = $ExtensionBlock; + } break; case ';': @@ -180,7 +216,11 @@ class getid3_gif extends getid3_handler return true; } - + /** + * @param int $bits + * + * @return float|int + */ public function GetLSBits($bits) { static $bitbuffer = ''; while (strlen($bitbuffer) < $bits) { diff --git a/lib/getid3/module.graphic.jpg.php b/lib/getid3/module.graphic.jpg.php index 76edf14..f557bf3 100644 --- a/lib/getid3/module.graphic.jpg.php +++ b/lib/getid3/module.graphic.jpg.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.jpg.php // @@ -15,11 +15,14 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_jpg extends getid3_handler { - - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -33,7 +36,7 @@ class getid3_jpg extends getid3_handler $imageinfo = array(); //list($width, $height, $type) = getid3_lib::GetDataImageSize($this->fread($info['filesize']), $imageinfo); - list($width, $height, $type) = getimagesize($info['filenamepath'], $imageinfo); // http://www.getid3.org/phpBB3/viewtopic.php?t=1474 + list($width, $height, $type) = getimagesize($info['filenamepath'], $imageinfo); // https://www.getid3.org/phpBB3/viewtopic.php?t=1474 if (isset($imageinfo['APP13'])) { @@ -59,26 +62,23 @@ class getid3_jpg extends getid3_handler $returnOK = false; switch ($type) { - case IMG_JPG: + case IMAGETYPE_JPEG: $info['video']['resolution_x'] = $width; $info['video']['resolution_y'] = $height; if (isset($imageinfo['APP1'])) { if (function_exists('exif_read_data')) { if (substr($imageinfo['APP1'], 0, 4) == 'Exif') { -//$this->warning('known issue: https://bugs.php.net/bug.php?id=62523'); -//return false; - set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { + //$this->warning('known issue: https://bugs.php.net/bug.php?id=62523'); + //return false; + set_error_handler(function($errno, $errstr, $errfile, $errline) { // https://github.com/JamesHeinrich/getID3/issues/275 if (!(error_reporting() & $errno)) { // error is not specified in the error_reporting setting, so we ignore it return false; } - - $errcontext['info']['warning'][] = 'Error parsing EXIF data ('.$errstr.')'; + $this->warning('Error parsing EXIF data ('.$errstr.')'); }); - $info['jpg']['exif'] = exif_read_data($info['filenamepath'], null, true, false); - restore_error_handler(); } else { $this->warning('exif_read_data() cannot parse non-EXIF data in APP1 (expected "Exif", found "'.substr($imageinfo['APP1'], 0, 4).'")'); @@ -108,6 +108,7 @@ class getid3_jpg extends getid3_handler if (isset($info['jpg']['exif']['GPS'])) { if (isset($info['jpg']['exif']['GPS']['GPSVersion'])) { + $version_subparts = array(); for ($i = 0; $i < 4; $i++) { $version_subparts[$i] = ord(substr($info['jpg']['exif']['GPS']['GPSVersion'], $i, 1)); } @@ -131,6 +132,7 @@ class getid3_jpg extends getid3_handler if (isset($info['jpg']['exif']['GPS']['GPSLatitude']) && is_array($info['jpg']['exif']['GPS']['GPSLatitude'])) { $direction_multiplier = ((isset($info['jpg']['exif']['GPS']['GPSLatitudeRef']) && ($info['jpg']['exif']['GPS']['GPSLatitudeRef'] == 'S')) ? -1 : 1); + $computed_latitude = array(); foreach ($info['jpg']['exif']['GPS']['GPSLatitude'] as $key => $value) { $computed_latitude[$key] = getid3_lib::DecimalizeFraction($value); } @@ -139,6 +141,7 @@ class getid3_jpg extends getid3_handler if (isset($info['jpg']['exif']['GPS']['GPSLongitude']) && is_array($info['jpg']['exif']['GPS']['GPSLongitude'])) { $direction_multiplier = ((isset($info['jpg']['exif']['GPS']['GPSLongitudeRef']) && ($info['jpg']['exif']['GPS']['GPSLongitudeRef'] == 'W')) ? -1 : 1); + $computed_longitude = array(); foreach ($info['jpg']['exif']['GPS']['GPSLongitude'] as $key => $value) { $computed_longitude[$key] = getid3_lib::DecimalizeFraction($value); } @@ -176,9 +179,13 @@ class getid3_jpg extends getid3_handler return true; } - + /** + * @param mixed $value + * + * @return mixed + */ public function CastAsAppropriate($value) { - if (is_array($value)) { + if (is_array($value) || is_null($value)) { return $value; } elseif (preg_match('#^[0-9]+/[0-9]+$#', $value)) { return getid3_lib::DecimalizeFraction($value); @@ -190,7 +197,11 @@ class getid3_jpg extends getid3_handler return $value; } - + /** + * @param int $iptc_record + * + * @return string + */ public function IPTCrecordName($iptc_record) { // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/IPTC.html static $IPTCrecordName = array(); @@ -207,7 +218,12 @@ class getid3_jpg extends getid3_handler return (isset($IPTCrecordName[$iptc_record]) ? $IPTCrecordName[$iptc_record] : ''); } - + /** + * @param int $iptc_record + * @param int $iptc_tagkey + * + * @return string + */ public function IPTCrecordTagName($iptc_record, $iptc_tagkey) { // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/IPTC.html static $IPTCrecordTagName = array(); diff --git a/lib/getid3/module.graphic.pcd.php b/lib/getid3/module.graphic.pcd.php index c1efaa2..6f2aaac 100644 --- a/lib/getid3/module.graphic.pcd.php +++ b/lib/getid3/module.graphic.pcd.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.pcd.php // @@ -14,11 +14,16 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_pcd extends getid3_handler { public $ExtractData = 0; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -45,9 +50,11 @@ class getid3_pcd extends getid3_handler if ($this->ExtractData > 3) { $this->error('Cannot extract PSD image data for detail levels above BASE (level-3) because encrypted with Kodak-proprietary compression/encryption.'); + return false; } elseif ($this->ExtractData > 0) { + $PCD_levels = array(); $PCD_levels[1] = array( 192, 128, 0x02000); // BASE/16 $PCD_levels[2] = array( 384, 256, 0x0B800); // BASE/4 $PCD_levels[3] = array( 768, 512, 0x30000); // BASE @@ -73,11 +80,11 @@ class getid3_pcd extends getid3_handler for ($x = 0; $x < $PCD_width; $x++) { if ($PCDisVertical) { - $info['pcd']['data'][$PCD_width - $x][$y] = $this->YCbCr2RGB(ord($PCD_data_Y1{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - $info['pcd']['data'][$PCD_width - $x][$y + 1] = $this->YCbCr2RGB(ord($PCD_data_Y2{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); + $info['pcd']['data'][$PCD_width - $x][$y] = $this->YCbCr2RGB(ord($PCD_data_Y1[$x]), ord($PCD_data_Cb[(int) floor($x / 2)]), ord($PCD_data_Cr[(int) floor($x / 2)])); + $info['pcd']['data'][$PCD_width - $x][$y + 1] = $this->YCbCr2RGB(ord($PCD_data_Y2[$x]), ord($PCD_data_Cb[(int) floor($x / 2)]), ord($PCD_data_Cr[(int) floor($x / 2)])); } else { - $info['pcd']['data'][$y][$x] = $this->YCbCr2RGB(ord($PCD_data_Y1{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - $info['pcd']['data'][$y + 1][$x] = $this->YCbCr2RGB(ord($PCD_data_Y2{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); + $info['pcd']['data'][$y][$x] = $this->YCbCr2RGB(ord($PCD_data_Y1[$x]), ord($PCD_data_Cb[(int) floor($x / 2)]), ord($PCD_data_Cr[(int) floor($x / 2)])); + $info['pcd']['data'][$y + 1][$x] = $this->YCbCr2RGB(ord($PCD_data_Y2[$x]), ord($PCD_data_Cb[(int) floor($x / 2)]), ord($PCD_data_Cr[(int) floor($x / 2)])); } } } @@ -97,8 +104,16 @@ class getid3_pcd extends getid3_handler } + return false; } + /** + * @param int $Y + * @param int $Cb + * @param int $Cr + * + * @return int + */ public function YCbCr2RGB($Y, $Cb, $Cr) { static $YCbCr_constants = array(); if (empty($YCbCr_constants)) { diff --git a/lib/getid3/module.graphic.png.php b/lib/getid3/module.graphic.png.php index 365d0d4..e29eeb7 100644 --- a/lib/getid3/module.graphic.png.php +++ b/lib/getid3/module.graphic.png.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.png.php // @@ -14,11 +14,23 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_png extends getid3_handler { - public $max_data_bytes = 10000000; // if data chunk is larger than this do not read it completely (getID3 only needs the first few dozen bytes for parsing) + /** + * If data chunk is larger than this do not read it completely (getID3 only needs the first + * few dozen bytes for parsing). + * + * @var int + */ + public $max_data_bytes = 10000000; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -45,6 +57,7 @@ class getid3_png extends getid3_handler } while ((($this->ftell() - (strlen($PNGfiledata) - $offset)) < $info['filesize'])) { + $chunk = array(); $chunk['data_length'] = getid3_lib::BigEndian2Int(substr($PNGfiledata, $offset, 4)); if ($chunk['data_length'] === false) { $this->error('Failed to read data_length at offset '.$offset); @@ -54,7 +67,13 @@ class getid3_png extends getid3_handler $truncated_data = false; while (((strlen($PNGfiledata) - $offset) < ($chunk['data_length'] + 4)) && ($this->ftell() < $info['filesize'])) { if (strlen($PNGfiledata) < $this->max_data_bytes) { - $PNGfiledata .= $this->fread($this->getid3->fread_buffer_size()); + $str = $this->fread($this->getid3->fread_buffer_size()); + if (strlen($str) > 0) { + $PNGfiledata .= $str; + } else { + $this->warning('At offset '.$offset.' chunk "'.substr($PNGfiledata, $offset, 4).'" no more data to read, data chunk will be truncated at '.(strlen($PNGfiledata) - 8).' bytes'); + break; + } } else { $this->warning('At offset '.$offset.' chunk "'.substr($PNGfiledata, $offset, 4).'" exceeded max_data_bytes value of '.$this->max_data_bytes.', data chunk will be truncated at '.(strlen($PNGfiledata) - 8).' bytes'); break; @@ -138,6 +157,7 @@ class getid3_png extends getid3_handler case 4: case 6: $this->error('Invalid color_type in tRNS chunk: '.$thisfile_png['IHDR']['raw']['color_type']); + break; default: $this->warning('Unhandled color_type in tRNS chunk: '.$thisfile_png['IHDR']['raw']['color_type']); @@ -492,6 +512,11 @@ class getid3_png extends getid3_handler return true; } + /** + * @param int $sRGB + * + * @return string + */ public function PNGsRGBintentLookup($sRGB) { static $PNGsRGBintentLookup = array( 0 => 'Perceptual', @@ -502,6 +527,11 @@ class getid3_png extends getid3_handler return (isset($PNGsRGBintentLookup[$sRGB]) ? $PNGsRGBintentLookup[$sRGB] : 'invalid'); } + /** + * @param int $compressionmethod + * + * @return string + */ public function PNGcompressionMethodLookup($compressionmethod) { static $PNGcompressionMethodLookup = array( 0 => 'deflate/inflate' @@ -509,6 +539,11 @@ class getid3_png extends getid3_handler return (isset($PNGcompressionMethodLookup[$compressionmethod]) ? $PNGcompressionMethodLookup[$compressionmethod] : 'invalid'); } + /** + * @param int $unitid + * + * @return string + */ public function PNGpHYsUnitLookup($unitid) { static $PNGpHYsUnitLookup = array( 0 => 'unknown', @@ -517,6 +552,11 @@ class getid3_png extends getid3_handler return (isset($PNGpHYsUnitLookup[$unitid]) ? $PNGpHYsUnitLookup[$unitid] : 'invalid'); } + /** + * @param int $unitid + * + * @return string + */ public function PNGoFFsUnitLookup($unitid) { static $PNGoFFsUnitLookup = array( 0 => 'pixel', @@ -525,6 +565,11 @@ class getid3_png extends getid3_handler return (isset($PNGoFFsUnitLookup[$unitid]) ? $PNGoFFsUnitLookup[$unitid] : 'invalid'); } + /** + * @param int $equationtype + * + * @return string + */ public function PNGpCALequationTypeLookup($equationtype) { static $PNGpCALequationTypeLookup = array( 0 => 'Linear mapping', @@ -535,6 +580,11 @@ class getid3_png extends getid3_handler return (isset($PNGpCALequationTypeLookup[$equationtype]) ? $PNGpCALequationTypeLookup[$equationtype] : 'invalid'); } + /** + * @param int $unitid + * + * @return string + */ public function PNGsCALUnitLookup($unitid) { static $PNGsCALUnitLookup = array( 0 => 'meter', @@ -543,27 +593,28 @@ class getid3_png extends getid3_handler return (isset($PNGsCALUnitLookup[$unitid]) ? $PNGsCALUnitLookup[$unitid] : 'invalid'); } + /** + * @param int $color_type + * @param int $bit_depth + * + * @return int|false + */ public function IHDRcalculateBitsPerSample($color_type, $bit_depth) { switch ($color_type) { case 0: // Each pixel is a grayscale sample. return $bit_depth; - break; case 2: // Each pixel is an R,G,B triple return 3 * $bit_depth; - break; case 3: // Each pixel is a palette index; a PLTE chunk must appear. return $bit_depth; - break; case 4: // Each pixel is a grayscale sample, followed by an alpha sample. return 2 * $bit_depth; - break; case 6: // Each pixel is an R,G,B triple, followed by an alpha sample. return 4 * $bit_depth; - break; } return false; } diff --git a/lib/getid3/module.graphic.svg.php b/lib/getid3/module.graphic.svg.php index e4502aa..8bb577b 100644 --- a/lib/getid3/module.graphic.svg.php +++ b/lib/getid3/module.graphic.svg.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.graphic.svg.php // @@ -14,11 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_svg extends getid3_handler { - - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; diff --git a/lib/getid3/module.graphic.tiff.php b/lib/getid3/module.graphic.tiff.php index 90dcb5c..e4f3cf7 100644 --- a/lib/getid3/module.graphic.tiff.php +++ b/lib/getid3/module.graphic.tiff.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.archive.tiff.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_tiff extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -34,14 +39,13 @@ class getid3_tiff extends getid3_handler default: $this->error('Invalid TIFF byte order identifier ('.substr($TIFFheader, 0, 2).') at offset '.$info['avdataoffset']); return false; - break; } $info['fileformat'] = 'tiff'; $info['video']['dataformat'] = 'tiff'; $info['video']['lossless'] = true; $info['tiff']['ifd'] = array(); - $CurrentIFD = array(); + $CurrentIFD = array(); $FieldTypeByteLength = array(1=>1, 2=>1, 3=>2, 4=>4, 5=>8); @@ -55,45 +59,61 @@ class getid3_tiff extends getid3_handler $CurrentIFD['fieldcount'] = $this->TIFFendian2Int($this->fread(2), $info['tiff']['byte_order']); for ($i = 0; $i < $CurrentIFD['fieldcount']; $i++) { - $CurrentIFD['fields'][$i]['raw']['tag'] = $this->TIFFendian2Int($this->fread(2), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['type'] = $this->TIFFendian2Int($this->fread(2), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['length'] = $this->TIFFendian2Int($this->fread(4), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['offset'] = $this->fread(4); + $CurrentIFD['fields'][$i]['raw']['tag'] = $this->TIFFendian2Int($this->fread(2), $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['raw']['type'] = $this->TIFFendian2Int($this->fread(2), $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['raw']['length'] = $this->TIFFendian2Int($this->fread(4), $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['raw']['valoff'] = $this->fread(4); // To save time and space the Value Offset contains the Value instead of pointing to the Value if and only if the Value fits into 4 bytes. If the Value is shorter than 4 bytes, it is left-justified within the 4-byte Value Offset, i.e., stored in the lowernumbered bytes. Whether the Value fits within 4 bytes is determined by the Type and Count of the field. + $CurrentIFD['fields'][$i]['raw']['tag_name'] = $this->TIFFcommentName($CurrentIFD['fields'][$i]['raw']['tag']); switch ($CurrentIFD['fields'][$i]['raw']['type']) { case 1: // BYTE An 8-bit unsigned integer. if ($CurrentIFD['fields'][$i]['raw']['length'] <= 4) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['offset'], 0, 1), $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['valoff'], 0, 1), $info['tiff']['byte_order']); } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); } break; case 2: // ASCII 8-bit bytes that store ASCII codes; the last byte must be null. if ($CurrentIFD['fields'][$i]['raw']['length'] <= 4) { - $CurrentIFD['fields'][$i]['value'] = substr($CurrentIFD['fields'][$i]['raw']['offset'], 3); + $CurrentIFD['fields'][$i]['value'] = substr($CurrentIFD['fields'][$i]['raw']['valoff'], 3); } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); } break; case 3: // SHORT A 16-bit (2-byte) unsigned integer. if ($CurrentIFD['fields'][$i]['raw']['length'] <= 2) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['offset'], 0, 2), $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['valoff'], 0, 2), $info['tiff']['byte_order']); } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); } break; case 4: // LONG A 32-bit (4-byte) unsigned integer. - if ($CurrentIFD['fields'][$i]['raw']['length'] <= 1) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); + if ($CurrentIFD['fields'][$i]['raw']['length'] <= 4) { + $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); + $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); } break; case 5: // RATIONAL Two LONG_s: the first represents the numerator of a fraction, the second the denominator. + case 7: // UNDEFINED An 8-bit byte that may contain anything, depending on the definition of the field. + $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['valoff'], $info['tiff']['byte_order']); + break; + + // Warning: It is possible that other TIFF field types will be added in the future. Readers should skip over fields containing an unexpected field type. + // In TIFF 6.0, some new field types have been defined: + // These new field types are also governed by the byte order (II or MM) in the TIFF header. + case 6: // SBYTE An 8-bit signed (twos-complement) integer. + case 8: // SSHORT A 16-bit (2-byte) signed (twos-complement) integer. + case 9: // SLONG A 32-bit (4-byte) signed (twos-complement) integer. + case 10: // SRATIONAL Two SLONGs: the first represents the numerator of a fraction, the second the denominator. + case 11: // FLOAT Single precision (4-byte) IEEE format + case 12: // DOUBLE Double precision (8-byte) IEEE format + default: + $this->warning('unhandled IFD field type '.$CurrentIFD['fields'][$i]['raw']['type'].' for IFD entry '.$i); break; } } @@ -105,6 +125,10 @@ class getid3_tiff extends getid3_handler } foreach ($info['tiff']['ifd'] as $IFDid => $IFDarray) { + if(!isset($IFDarray['fields'])) { + continue; + } + foreach ($IFDarray['fields'] as $key => $fieldarray) { switch ($fieldarray['raw']['tag']) { case 256: // ImageWidth @@ -133,6 +157,16 @@ class getid3_tiff extends getid3_handler } break; + case 700: + $XMPmagic = 'fseek($fieldarray['offset']); + $xmpkey = (isset($info['tiff']['XMP']) ? count($info['tiff']['XMP']) : 0); + $info['tiff']['XMP'][$xmpkey]['raw'] = $this->fread($fieldarray['raw']['length']); + if (substr($info['tiff']['XMP'][$xmpkey]['raw'], 0, strlen($XMPmagic)) != $XMPmagic) { + $this->warning('did not find expected XMP data at offset '.$fieldarray['offset']); + unset($info['tiff']['XMP'][$xmpkey]['raw']); + } + break; } switch ($fieldarray['raw']['tag']) { case 256: // ImageWidth @@ -165,7 +199,7 @@ class getid3_tiff extends getid3_handler case 306: // DateTime case 315: // Artist case 316: // HostComputer - $TIFFcommentName = $this->TIFFcommentName($fieldarray['raw']['tag']); + $TIFFcommentName = strtolower($fieldarray['raw']['tag_name']); if (isset($info['tiff']['comments'][$TIFFcommentName])) { $info['tiff']['comments'][$TIFFcommentName][] = $info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data']; } else { @@ -182,7 +216,12 @@ class getid3_tiff extends getid3_handler return true; } - + /** + * @param string $bytestring + * @param string $byteorder + * + * @return int|float|false + */ public function TIFFendian2Int($bytestring, $byteorder) { if ($byteorder == 'Intel') { return getid3_lib::LittleEndian2Int($bytestring); @@ -192,34 +231,290 @@ class getid3_tiff extends getid3_handler return false; } + /** + * @param int $id + * + * @return string + */ public function TIFFcompressionMethod($id) { + // https://en.wikipedia.org/wiki/TIFF#TIFF_Compression_Tag static $TIFFcompressionMethod = array(); if (empty($TIFFcompressionMethod)) { $TIFFcompressionMethod = array( - 1 => 'Uncompressed', - 2 => 'Huffman', - 3 => 'Fax - CCITT 3', - 5 => 'LZW', - 32773 => 'PackBits', + 0x0001 => 'Uncompressed', + 0x0002 => 'Huffman', + 0x0003 => 'CCITT T.4', + 0x0004 => 'CCITT T.6', + 0x0005 => 'LZW', + 0x0006 => 'JPEG-old', + 0x0007 => 'JPEG', + 0x0008 => 'deflate', + 0x0009 => 'JBIG ITU-T T.85', + 0x000A => 'JBIG ITU-T T.43', + 0x7FFE => 'NeXT RLE 2-bit', + 0x8005 => 'PackBits', + 0x8029 => 'ThunderScan RLE 4-bit', + 0x807F => 'RasterPadding', + 0x8080 => 'RLE-LW', + 0x8081 => 'RLE-CT', + 0x8082 => 'RLE-BL', + 0x80B2 => 'deflate-PK', + 0x80B3 => 'Kodak-DCS', + 0x8765 => 'JBIG', + 0x8798 => 'JPEG2000', + 0x8799 => 'Nikon NEF', + 0x879B => 'JBIG2', ); } return (isset($TIFFcompressionMethod[$id]) ? $TIFFcompressionMethod[$id] : 'unknown/invalid ('.$id.')'); } + /** + * @param int $id + * + * @return string + */ public function TIFFcommentName($id) { + // https://www.awaresystems.be/imaging/tiff/tifftags.html static $TIFFcommentName = array(); if (empty($TIFFcommentName)) { $TIFFcommentName = array( - 270 => 'imagedescription', - 271 => 'make', - 272 => 'model', - 305 => 'software', - 306 => 'datetime', - 315 => 'artist', - 316 => 'hostcomputer', + 254 => 'NewSubfileType', + 255 => 'SubfileType', + 256 => 'ImageWidth', + 257 => 'ImageLength', + 258 => 'BitsPerSample', + 259 => 'Compression', + 262 => 'PhotometricInterpretation', + 263 => 'Threshholding', + 264 => 'CellWidth', + 265 => 'CellLength', + 266 => 'FillOrder', + 269 => 'DocumentName', + 270 => 'ImageDescription', + 271 => 'Make', + 272 => 'Model', + 273 => 'StripOffsets', + 274 => 'Orientation', + 277 => 'SamplesPerPixel', + 278 => 'RowsPerStrip', + 279 => 'StripByteCounts', + 280 => 'MinSampleValue', + 281 => 'MaxSampleValue', + 282 => 'XResolution', + 283 => 'YResolution', + 284 => 'PlanarConfiguration', + 285 => 'PageName', + 286 => 'XPosition', + 287 => 'YPosition', + 288 => 'FreeOffsets', + 289 => 'FreeByteCounts', + 290 => 'GrayResponseUnit', + 291 => 'GrayResponseCurve', + 292 => 'T4Options', + 293 => 'T6Options', + 296 => 'ResolutionUnit', + 297 => 'PageNumber', + 301 => 'TransferFunction', + 305 => 'Software', + 306 => 'DateTime', + 315 => 'Artist', + 316 => 'HostComputer', + 317 => 'Predictor', + 318 => 'WhitePoint', + 319 => 'PrimaryChromaticities', + 320 => 'ColorMap', + 321 => 'HalftoneHints', + 322 => 'TileWidth', + 323 => 'TileLength', + 324 => 'TileOffsets', + 325 => 'TileByteCounts', + 326 => 'BadFaxLines', + 327 => 'CleanFaxData', + 328 => 'ConsecutiveBadFaxLines', + 330 => 'SubIFDs', + 332 => 'InkSet', + 333 => 'InkNames', + 334 => 'NumberOfInks', + 336 => 'DotRange', + 337 => 'TargetPrinter', + 338 => 'ExtraSamples', + 339 => 'SampleFormat', + 340 => 'SMinSampleValue', + 341 => 'SMaxSampleValue', + 342 => 'TransferRange', + 343 => 'ClipPath', + 344 => 'XClipPathUnits', + 345 => 'YClipPathUnits', + 346 => 'Indexed', + 347 => 'JPEGTables', + 351 => 'OPIProxy', + 400 => 'GlobalParametersIFD', + 401 => 'ProfileType', + 402 => 'FaxProfile', + 403 => 'CodingMethods', + 404 => 'VersionYear', + 405 => 'ModeNumber', + 433 => 'Decode', + 434 => 'DefaultImageColor', + 512 => 'JPEGProc', + 513 => 'JPEGInterchangeFormat', + 514 => 'JPEGInterchangeFormatLngth', + 515 => 'JPEGRestartInterval', + 517 => 'JPEGLosslessPredictors', + 518 => 'JPEGPointTransforms', + 519 => 'JPEGQTables', + 520 => 'JPEGDCTables', + 521 => 'JPEGACTables', + 529 => 'YCbCrCoefficients', + 530 => 'YCbCrSubSampling', + 531 => 'YCbCrPositioning', + 532 => 'ReferenceBlackWhite', + 559 => 'StripRowCounts', + 700 => 'XMP', + + 32781 => 'ImageID', + 33432 => 'Copyright', + 34732 => 'ImageLayer', + + // Private Tags - https://www.awaresystems.be/imaging/tiff/tifftags/private.html + 32932 => 'Wang Annotation', // Annotation data, as used in 'Imaging for Windows'. + 33445 => 'MD FileTag', // Specifies the pixel data format encoding in the Molecular Dynamics GEL file format. + 33446 => 'MD ScalePixel', // Specifies a scale factor in the Molecular Dynamics GEL file format. + 33447 => 'MD ColorTable', // Used to specify the conversion from 16bit to 8bit in the Molecular Dynamics GEL file format. + 33448 => 'MD LabName', // Name of the lab that scanned this file, as used in the Molecular Dynamics GEL file format. + 33449 => 'MD SampleInfo', // Information about the sample, as used in the Molecular Dynamics GEL file format. + 33450 => 'MD PrepDate', // Date the sample was prepared, as used in the Molecular Dynamics GEL file format. + 33451 => 'MD PrepTime', // Time the sample was prepared, as used in the Molecular Dynamics GEL file format. + 33452 => 'MD FileUnits', // Units for data in this file, as used in the Molecular Dynamics GEL file format. + 33550 => 'ModelPixelScaleTag', // Used in interchangeable GeoTIFF files. + 33723 => 'IPTC', // IPTC (International Press Telecommunications Council) metadata. + 33918 => 'INGR Packet Data Tag', // Intergraph Application specific storage. + 33919 => 'INGR Flag Registers', // Intergraph Application specific flags. + 33920 => 'IrasB Transformation Matrix', // Originally part of Intergraph's GeoTIFF tags, but likely understood by IrasB only. + 33922 => 'ModelTiepointTag', // Originally part of Intergraph's GeoTIFF tags, but now used in interchangeable GeoTIFF files. + 34264 => 'ModelTransformationTag', // Used in interchangeable GeoTIFF files. + 34377 => 'Photoshop', // Collection of Photoshop 'Image Resource Blocks'. + 34665 => 'Exif IFD', // A pointer to the Exif IFD. + 34675 => 'ICC Profile', // ICC profile data. + 34735 => 'GeoKeyDirectoryTag', // Used in interchangeable GeoTIFF files. + 34736 => 'GeoDoubleParamsTag', // Used in interchangeable GeoTIFF files. + 34737 => 'GeoAsciiParamsTag', // Used in interchangeable GeoTIFF files. + 34853 => 'GPS IFD', // A pointer to the Exif-related GPS Info IFD. + 34908 => 'HylaFAX FaxRecvParams', // Used by HylaFAX. + 34909 => 'HylaFAX FaxSubAddress', // Used by HylaFAX. + 34910 => 'HylaFAX FaxRecvTime', // Used by HylaFAX. + 37724 => 'ImageSourceData', // Used by Adobe Photoshop. + 40965 => 'Interoperability IFD', // A pointer to the Exif-related Interoperability IFD. + 42112 => 'GDAL_METADATA', // Used by the GDAL library, holds an XML list of name=value 'metadata' values about the image as a whole, and about specific samples. + 42113 => 'GDAL_NODATA', // Used by the GDAL library, contains an ASCII encoded nodata or background pixel value. + 50215 => 'Oce Scanjob Description', // Used in the Oce scanning process. + 50216 => 'Oce Application Selector', // Used in the Oce scanning process. + 50217 => 'Oce Identification Number', // Used in the Oce scanning process. + 50218 => 'Oce ImageLogic Characteristics', // Used in the Oce scanning process. + 50706 => 'DNGVersion', // Used in IFD 0 of DNG files. + 50707 => 'DNGBackwardVersion', // Used in IFD 0 of DNG files. + 50708 => 'UniqueCameraModel', // Used in IFD 0 of DNG files. + 50709 => 'LocalizedCameraModel', // Used in IFD 0 of DNG files. + 50710 => 'CFAPlaneColor', // Used in Raw IFD of DNG files. + 50711 => 'CFALayout', // Used in Raw IFD of DNG files. + 50712 => 'LinearizationTable', // Used in Raw IFD of DNG files. + 50713 => 'BlackLevelRepeatDim', // Used in Raw IFD of DNG files. + 50714 => 'BlackLevel', // Used in Raw IFD of DNG files. + 50715 => 'BlackLevelDeltaH', // Used in Raw IFD of DNG files. + 50716 => 'BlackLevelDeltaV', // Used in Raw IFD of DNG files. + 50717 => 'WhiteLevel', // Used in Raw IFD of DNG files. + 50718 => 'DefaultScale', // Used in Raw IFD of DNG files. + 50719 => 'DefaultCropOrigin', // Used in Raw IFD of DNG files. + 50720 => 'DefaultCropSize', // Used in Raw IFD of DNG files. + 50721 => 'ColorMatrix1', // Used in IFD 0 of DNG files. + 50722 => 'ColorMatrix2', // Used in IFD 0 of DNG files. + 50723 => 'CameraCalibration1', // Used in IFD 0 of DNG files. + 50724 => 'CameraCalibration2', // Used in IFD 0 of DNG files. + 50725 => 'ReductionMatrix1', // Used in IFD 0 of DNG files. + 50726 => 'ReductionMatrix2', // Used in IFD 0 of DNG files. + 50727 => 'AnalogBalance', // Used in IFD 0 of DNG files. + 50728 => 'AsShotNeutral', // Used in IFD 0 of DNG files. + 50729 => 'AsShotWhiteXY', // Used in IFD 0 of DNG files. + 50730 => 'BaselineExposure', // Used in IFD 0 of DNG files. + 50731 => 'BaselineNoise', // Used in IFD 0 of DNG files. + 50732 => 'BaselineSharpness', // Used in IFD 0 of DNG files. + 50733 => 'BayerGreenSplit', // Used in Raw IFD of DNG files. + 50734 => 'LinearResponseLimit', // Used in IFD 0 of DNG files. + 50735 => 'CameraSerialNumber', // Used in IFD 0 of DNG files. + 50736 => 'LensInfo', // Used in IFD 0 of DNG files. + 50737 => 'ChromaBlurRadius', // Used in Raw IFD of DNG files. + 50738 => 'AntiAliasStrength', // Used in Raw IFD of DNG files. + 50740 => 'DNGPrivateData', // Used in IFD 0 of DNG files. + 50741 => 'MakerNoteSafety', // Used in IFD 0 of DNG files. + 50778 => 'CalibrationIlluminant1', // Used in IFD 0 of DNG files. + 50779 => 'CalibrationIlluminant2', // Used in IFD 0 of DNG files. + 50780 => 'BestQualityScale', // Used in Raw IFD of DNG files. + 50784 => 'Alias Layer Metadata', // Alias Sketchbook Pro layer usage description. + 50908 => 'TIFF_RSID', // This private tag is used in a GEOTIFF standard by DGIWG. + 50909 => 'GEO_METADATA', // This private tag is used in a GEOTIFF standard by DGIWG. + + // EXIF tags - https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html + 33434 => 'ExposureTime', // Exposure time, given in seconds. + 33437 => 'FNumber', // The F number. + 34850 => 'ExposureProgram', // The class of the program used by the camera to set exposure when the picture is taken. + 34852 => 'SpectralSensitivity', // Indicates the spectral sensitivity of each channel of the camera used. + 34855 => 'ISOSpeedRatings', // Indicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232. + 34856 => 'OECF', // Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. + 36864 => 'ExifVersion', // The version of the supported Exif standard. + 36867 => 'DateTimeOriginal', // The date and time when the original image data was generated. + 36868 => 'DateTimeDigitized', // The date and time when the image was stored as digital data. + 37121 => 'ComponentsConfiguration', // Specific to compressed data; specifies the channels and complements PhotometricInterpretation + 37122 => 'CompressedBitsPerPixel', // Specific to compressed data; states the compressed bits per pixel. + 37377 => 'ShutterSpeedValue', // Shutter speed. + 37378 => 'ApertureValue', // The lens aperture. + 37379 => 'BrightnessValue', // The value of brightness. + 37380 => 'ExposureBiasValue', // The exposure bias. + 37381 => 'MaxApertureValue', // The smallest F number of the lens. + 37382 => 'SubjectDistance', // The distance to the subject, given in meters. + 37383 => 'MeteringMode', // The metering mode. + 37384 => 'LightSource', // The kind of light source. + 37385 => 'Flash', // Indicates the status of flash when the image was shot. + 37386 => 'FocalLength', // The actual focal length of the lens, in mm. + 37396 => 'SubjectArea', // Indicates the location and area of the main subject in the overall scene. + 37500 => 'MakerNote', // Manufacturer specific information. + 37510 => 'UserComment', // Keywords or comments on the image; complements ImageDescription. + 37520 => 'SubsecTime', // A tag used to record fractions of seconds for the DateTime tag. + 37521 => 'SubsecTimeOriginal', // A tag used to record fractions of seconds for the DateTimeOriginal tag. + 37522 => 'SubsecTimeDigitized', // A tag used to record fractions of seconds for the DateTimeDigitized tag. + 40960 => 'FlashpixVersion', // The Flashpix format version supported by a FPXR file. + 40961 => 'ColorSpace', // The color space information tag is always recorded as the color space specifier. + 40962 => 'PixelXDimension', // Specific to compressed data; the valid width of the meaningful image. + 40963 => 'PixelYDimension', // Specific to compressed data; the valid height of the meaningful image. + 40964 => 'RelatedSoundFile', // Used to record the name of an audio file related to the image data. + 41483 => 'FlashEnergy', // Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds + 41484 => 'SpatialFrequencyResponse', // Records the camera or input device spatial frequency table and SFR values in the direction of image width, image height, and diagonal direction, as specified in ISO 12233. + 41486 => 'FocalPlaneXResolution', // Indicates the number of pixels in the image width (X) direction per FocalPlaneResolutionUnit on the camera focal plane. + 41487 => 'FocalPlaneYResolution', // Indicates the number of pixels in the image height (Y) direction per FocalPlaneResolutionUnit on the camera focal plane. + 41488 => 'FocalPlaneResolutionUnit', // Indicates the unit for measuring FocalPlaneXResolution and FocalPlaneYResolution. + 41492 => 'SubjectLocation', // Indicates the location of the main subject in the scene. + 41493 => 'ExposureIndex', // Indicates the exposure index selected on the camera or input device at the time the image is captured. + 41495 => 'SensingMethod', // Indicates the image sensor type on the camera or input device. + 41728 => 'FileSource', // Indicates the image source. + 41729 => 'SceneType', // Indicates the type of scene. + 41730 => 'CFAPattern', // Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. + 41985 => 'CustomRendered', // Indicates the use of special processing on image data, such as rendering geared to output. + 41986 => 'ExposureMode', // Indicates the exposure mode set when the image was shot. + 41987 => 'WhiteBalance', // Indicates the white balance mode set when the image was shot. + 41988 => 'DigitalZoomRatio', // Indicates the digital zoom ratio when the image was shot. + 41989 => 'FocalLengthIn35mmFilm', // Indicates the equivalent focal length assuming a 35mm film camera, in mm. + 41990 => 'SceneCaptureType', // Indicates the type of scene that was shot. + 41991 => 'GainControl', // Indicates the degree of overall image gain adjustment. + 41992 => 'Contrast', // Indicates the direction of contrast processing applied by the camera when the image was shot. + 41993 => 'Saturation', // Indicates the direction of saturation processing applied by the camera when the image was shot. + 41994 => 'Sharpness', // Indicates the direction of sharpness processing applied by the camera when the image was shot. + 41995 => 'DeviceSettingDescription', // This tag indicates information on the picture-taking conditions of a particular camera model. + 41996 => 'SubjectDistanceRange', // Indicates the distance to the subject. + 42016 => 'ImageUniqueID', // Indicates an identifier assigned uniquely to each image. ); } return (isset($TIFFcommentName[$id]) ? $TIFFcommentName[$id] : 'unknown/invalid ('.$id.')'); } + } diff --git a/lib/getid3/module.misc.cue.php b/lib/getid3/module.misc.cue.php index 9c42799..b358f8a 100644 --- a/lib/getid3/module.misc.cue.php +++ b/lib/getid3/module.misc.cue.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.cue.php // @@ -31,10 +31,18 @@ * A CueSheet class used to open and parse cuesheets. * */ + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + class getid3_cue extends getid3_handler { public $cuesheet = array(); + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -44,22 +52,28 @@ class getid3_cue extends getid3_handler return true; } - - + /** + * @param string $filename + * + * @return array + */ public function readCueSheetFilename($filename) { $filedata = file_get_contents($filename); return $this->readCueSheet($filedata); } + /** - * Parses a cue sheet file. - * - * @param string $filename - The filename for the cue sheet to open. - */ + * Parses a cue sheet file. + * + * @param string $filedata + * + * @return array + */ public function readCueSheet(&$filedata) { $cue_lines = array(); - foreach (explode("\n", str_replace("\r", null, $filedata)) as $line) + foreach (explode("\n", str_replace("\r", '', $filedata)) as $line) { if ( (strlen($line) > 0) && ($line[0] != '#')) { @@ -72,18 +86,18 @@ class getid3_cue extends getid3_handler } /** - * Parses the cue sheet array. - * - * @param array $file - The cuesheet as an array of each line. - */ + * Parses the cue sheet array. + * + * @param array $file - The cuesheet as an array of each line. + */ public function parseCueSheet($file) { //-1 means still global, all others are track specific $track_on = -1; + $currentFile = null; - for ($i=0; $i < count($file); $i++) - { - list($key) = explode(' ', strtolower($file[$i]), 2); + foreach ($file as $line) { + list($key) = explode(' ', strtolower($line), 2); switch ($key) { case 'catalog': @@ -92,25 +106,25 @@ class getid3_cue extends getid3_handler case 'performer': case 'songwriter': case 'title': - $this->parseString($file[$i], $track_on); + $this->parseString($line, $track_on); break; case 'file': - $currentFile = $this->parseFile($file[$i]); + $currentFile = $this->parseFile($line); break; case 'flags': - $this->parseFlags($file[$i], $track_on); + $this->parseFlags($line, $track_on); break; case 'index': case 'postgap': case 'pregap': - $this->parseIndex($file[$i], $track_on); + $this->parseIndex($line, $track_on); break; case 'rem': - $this->parseComment($file[$i], $track_on); + $this->parseComment($line, $track_on); break; case 'track': $track_on++; - $this->parseTrack($file[$i], $track_on); + $this->parseTrack($line, $track_on); if (isset($currentFile)) // if there's a file { $this->cuesheet['tracks'][$track_on]['datafile'] = $currentFile; @@ -118,18 +132,18 @@ class getid3_cue extends getid3_handler break; default: //save discarded junk and place string[] with track it was found in - $this->parseGarbage($file[$i], $track_on); + $this->parseGarbage($line, $track_on); break; } } } /** - * Parses the REM command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ + * Parses the REM command. + * + * @param string $line - The line in the cue file that contains the TRACK command. + * @param integer $track_on - The track currently processing. + */ public function parseComment($line, $track_on) { $explodedline = explode(' ', $line, 3); @@ -148,11 +162,12 @@ class getid3_cue extends getid3_handler } /** - * Parses the FILE command. - * - * @param string $line - The line in the cue file that contains the FILE command. - * @return array - Array of FILENAME and TYPE of file.. - */ + * Parses the FILE command. + * + * @param string $line - The line in the cue file that contains the FILE command. + * + * @return array - Array of FILENAME and TYPE of file.. + */ public function parseFile($line) { $line = substr($line, strpos($line, ' ') + 1); @@ -168,11 +183,11 @@ class getid3_cue extends getid3_handler } /** - * Parses the FLAG command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ + * Parses the FLAG command. + * + * @param string $line - The line in the cue file that contains the TRACK command. + * @param integer $track_on - The track currently processing. + */ public function parseFlags($line, $track_on) { if ($track_on != -1) @@ -206,11 +221,11 @@ class getid3_cue extends getid3_handler } /** - * Collect any unidentified data. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ + * Collect any unidentified data. + * + * @param string $line - The line in the cue file that contains the TRACK command. + * @param integer $track_on - The track currently processing. + */ public function parseGarbage($line, $track_on) { if ( strlen($line) > 0 ) @@ -227,15 +242,16 @@ class getid3_cue extends getid3_handler } /** - * Parses the INDEX command of a TRACK. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ + * Parses the INDEX command of a TRACK. + * + * @param string $line - The line in the cue file that contains the TRACK command. + * @param integer $track_on - The track currently processing. + */ public function parseIndex($line, $track_on) { $type = strtolower(substr($line, 0, strpos($line, ' '))); $line = substr($line, strpos($line, ' ') + 1); + $number = 0; if ($type == 'index') { @@ -261,6 +277,10 @@ class getid3_cue extends getid3_handler } } + /** + * @param string $line + * @param int $track_on + */ public function parseString($line, $track_on) { $category = strtolower(substr($line, 0, strpos($line, ' '))); @@ -292,11 +312,11 @@ class getid3_cue extends getid3_handler } /** - * Parses the TRACK command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ + * Parses the TRACK command. + * + * @param string $line - The line in the cue file that contains the TRACK command. + * @param integer $track_on - The track currently processing. + */ public function parseTrack($line, $track_on) { $line = substr($line, strpos($line, ' ') + 1); diff --git a/lib/getid3/module.misc.exe.php b/lib/getid3/module.misc.exe.php index 4f04ad7..9064fed 100644 --- a/lib/getid3/module.misc.exe.php +++ b/lib/getid3/module.misc.exe.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.exe.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_exe extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -51,8 +56,8 @@ class getid3_exe extends getid3_handler $info['exe']['mz']['memory_minimum'] = $info['exe']['mz']['raw']['min_memory_paragraphs'] * 16; $info['exe']['mz']['memory_recommended'] = $info['exe']['mz']['raw']['max_memory_paragraphs'] * 16; -$this->error('EXE parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; + $this->error('EXE parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; } diff --git a/lib/getid3/module.misc.iso.php b/lib/getid3/module.misc.iso.php index 0477285..939bd63 100644 --- a/lib/getid3/module.misc.iso.php +++ b/lib/getid3/module.misc.iso.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.iso.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_iso extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -27,7 +32,7 @@ class getid3_iso extends getid3_handler $this->fseek(2048 * $i); $ISOheader = $this->fread(2048); if (substr($ISOheader, 1, 5) == 'CD001') { - switch (ord($ISOheader{0})) { + switch (ord($ISOheader[0])) { case 1: $info['iso']['primary_volume_descriptor']['offset'] = 2048 * $i; $this->ParsePrimaryVolumeDescriptor($ISOheader); @@ -48,14 +53,20 @@ class getid3_iso extends getid3_handler $this->ParsePathTable(); $info['iso']['files'] = array(); - foreach ($info['iso']['path_table']['directories'] as $directorynum => $directorydata) { - $info['iso']['directories'][$directorynum] = $this->ParseDirectoryRecord($directorydata); + if (!empty($info['iso']['path_table']['directories'])) { + foreach ($info['iso']['path_table']['directories'] as $directorynum => $directorydata) { + $info['iso']['directories'][$directorynum] = $this->ParseDirectoryRecord($directorydata); + } } return true; } - + /** + * @param string $ISOheader + * + * @return bool + */ public function ParsePrimaryVolumeDescriptor(&$ISOheader) { // ISO integer values are stored *BOTH* Little-Endian AND Big-Endian format!! // ie 12345 == 0x3039 is stored as $39 $30 $30 $39 in a 4-byte field @@ -129,7 +140,11 @@ class getid3_iso extends getid3_handler return true; } - + /** + * @param string $ISOheader + * + * @return bool + */ public function ParseSupplementaryVolumeDescriptor(&$ISOheader) { // ISO integer values are stored Both-Endian format!! // ie 12345 == 0x3039 is stored as $39 $30 $30 $39 in a 4-byte field @@ -208,7 +223,9 @@ class getid3_iso extends getid3_handler return true; } - + /** + * @return bool + */ public function ParsePathTable() { $info = &$this->getid3->info; if (!isset($info['iso']['supplementary_volume_descriptor']['raw']['path_table_l_location']) && !isset($info['iso']['primary_volume_descriptor']['raw']['path_table_l_location'])) { @@ -235,6 +252,7 @@ class getid3_iso extends getid3_handler $offset = 0; $pathcounter = 1; + $FullPathArray = array(); while ($offset < $PathTableSize) { // shortcut $info['iso']['path_table']['directories'][$pathcounter] = array(); @@ -267,7 +285,11 @@ class getid3_iso extends getid3_handler return true; } - + /** + * @param array $directorydata + * + * @return array + */ public function ParseDirectoryRecord($directorydata) { $info = &$this->getid3->info; if (isset($info['iso']['supplementary_volume_descriptor'])) { @@ -278,10 +300,13 @@ class getid3_iso extends getid3_handler $this->fseek($directorydata['location_bytes']); $DirectoryRecordData = $this->fread(1); + $DirectoryRecord = array(); - while (ord($DirectoryRecordData{0}) > 33) { + while (ord($DirectoryRecordData[0]) > 33) { - $DirectoryRecordData .= $this->fread(ord($DirectoryRecordData{0}) - 1); + $DirectoryRecordData .= $this->fread(ord($DirectoryRecordData[0]) - 1); + + $ThisDirectoryRecord = array(); $ThisDirectoryRecord['raw']['length'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 0, 1)); $ThisDirectoryRecord['raw']['extended_attribute_length'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 1, 1)); @@ -321,6 +346,11 @@ class getid3_iso extends getid3_handler return $DirectoryRecord; } + /** + * @param string $ISOfilename + * + * @return string + */ public function ISOstripFilenameVersion($ISOfilename) { // convert 'filename.ext;1' to 'filename.ext' if (!strstr($ISOfilename, ';')) { @@ -330,6 +360,11 @@ class getid3_iso extends getid3_handler } } + /** + * @param string $ISOtime + * + * @return int|false + */ public function ISOtimeText2UNIXtime($ISOtime) { $UNIXyear = (int) substr($ISOtime, 0, 4); @@ -345,6 +380,11 @@ class getid3_iso extends getid3_handler return gmmktime($UNIXhour, $UNIXminute, $UNIXsecond, $UNIXmonth, $UNIXday, $UNIXyear); } + /** + * @param string $ISOtime + * + * @return int + */ public function ISOtime2UNIXtime($ISOtime) { // Represented by seven bytes: // 1: Number of years since 1900 @@ -355,17 +395,22 @@ class getid3_iso extends getid3_handler // 6: second of the minute from 0 to 59 // 7: Offset from Greenwich Mean Time in number of 15 minute intervals from -48 (West) to +52 (East) - $UNIXyear = ord($ISOtime{0}) + 1900; - $UNIXmonth = ord($ISOtime{1}); - $UNIXday = ord($ISOtime{2}); - $UNIXhour = ord($ISOtime{3}); - $UNIXminute = ord($ISOtime{4}); - $UNIXsecond = ord($ISOtime{5}); - $GMToffset = $this->TwosCompliment2Decimal(ord($ISOtime{5})); + $UNIXyear = ord($ISOtime[0]) + 1900; + $UNIXmonth = ord($ISOtime[1]); + $UNIXday = ord($ISOtime[2]); + $UNIXhour = ord($ISOtime[3]); + $UNIXminute = ord($ISOtime[4]); + $UNIXsecond = ord($ISOtime[5]); + $GMToffset = $this->TwosCompliment2Decimal(ord($ISOtime[5])); return gmmktime($UNIXhour, $UNIXminute, $UNIXsecond, $UNIXmonth, $UNIXday, $UNIXyear); } + /** + * @param int $BinaryValue + * + * @return int + */ public function TwosCompliment2Decimal($BinaryValue) { // http://sandbox.mc.edu/~bennet/cs110/tc/tctod.html // First check if the number is negative or positive by looking at the sign bit. diff --git a/lib/getid3/module.misc.msoffice.php b/lib/getid3/module.misc.msoffice.php index 099e6fc..56d07f0 100644 --- a/lib/getid3/module.misc.msoffice.php +++ b/lib/getid3/module.misc.msoffice.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.msoffice.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_msoffice extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -30,8 +35,8 @@ class getid3_msoffice extends getid3_handler } $info['fileformat'] = 'msoffice'; -$this->error('MS Office (.doc, .xls, etc) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); -return false; + $this->error('MS Office (.doc, .xls, etc) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; } diff --git a/lib/getid3/module.misc.par2.php b/lib/getid3/module.misc.par2.php index 2312bb8..b6c493b 100644 --- a/lib/getid3/module.misc.par2.php +++ b/lib/getid3/module.misc.par2.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.par2.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_par2 extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; diff --git a/lib/getid3/module.misc.pdf.php b/lib/getid3/module.misc.pdf.php index e8d25cb..fd4ae72 100644 --- a/lib/getid3/module.misc.pdf.php +++ b/lib/getid3/module.misc.pdf.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.misc.pdf.php // @@ -14,18 +14,145 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_pdf extends getid3_handler { + /** misc.pdf + * return full details of PDF Cross-Reference Table (XREF) + * + * @var bool + */ + public $returnXREF = false; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; + $this->fseek(0); + if (preg_match('#^%PDF-([0-9\\.]+)$#', rtrim($this->fgets()), $matches)) { + $info['pdf']['header']['version'] = floatval($matches[1]); + $info['fileformat'] = 'pdf'; - $info['fileformat'] = 'pdf'; + // the PDF Cross-Reference Table (XREF) is located near the end of the file + // the starting offset is specified in the penultimate section, on the two lines just before "%%EOF" + // the first line is "startxref", the second line is the byte offset of the XREF. + // We know the length of "%%EOF" and "startxref", but the offset could be 2-10 bytes, + // and we're not sure if the line ends are one or two bytes, so we might find "startxref" as little as 18(?) bytes + // from EOF, but it could 30 bytes, so we start 40 bytes back just to be safe and do a search for the data we want. + $this->fseek(-40, SEEK_END); + if (preg_match('#[\r\n]startxref[ \r\n]+([0-9]+)[ \r\n]+#', $this->fread(40), $matches)) { + $info['pdf']['trailer']['startxref'] = intval($matches[1]); + $this->parseXREF($info['pdf']['trailer']['startxref']); + if (!empty($info['pdf']['xref']['offset'])) { + while (!$this->feof() && (max(array_keys($info['pdf']['xref']['offset'])) > $info['pdf']['xref']['count'])) { + // suspect that there may be another XREF entry somewhere in the file, brute-force scan for it + /* + // starting at last known entry of main XREF table + $this->fseek(max($info['pdf']['xref']['offset'])); + */ + // starting at the beginning of the file + $this->fseek(0); + while (!$this->feof()) { + $XREFoffset = $this->ftell(); + if (rtrim($this->fgets()) == 'xref') { + if (empty($info['pdf']['xref']['xref_offsets']) || !in_array($XREFoffset, $info['pdf']['xref']['xref_offsets'])) { + $this->parseXREF($XREFoffset); + break; + } + } + } + } - $this->error('PDF parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + asort($info['pdf']['xref']['offset']); + $maxObjLengths = array(); + $prevOffset = 0; + $prevObjNum = 0; + foreach ($info['pdf']['xref']['offset'] as $objectNumber => $offset) { + // walk through all listed offsets to calculate the maximum possible length for each known object + if ($prevObjNum) { + $maxObjLengths[$prevObjNum] = $offset - $prevOffset; + } + $prevOffset = $offset; + $prevObjNum = $objectNumber; + } + ksort($maxObjLengths); + foreach ($info['pdf']['xref']['offset'] as $objectNumber => $offset) { + if ($info['pdf']['xref']['entry'][$objectNumber] == 'f') { + // "free" object means "deleted", ignore + continue; + } + if (!empty($maxObjLengths[$objectNumber]) && ($maxObjLengths[$objectNumber] < $this->getid3->option_fread_buffer_size)) { + // ignore object that are zero-size or >32kB, they are unlikely to contain information we're interested in + $this->fseek($offset); + $objBlob = $this->fread($maxObjLengths[$objectNumber]); + if (preg_match('#^'.$objectNumber.'[\\x00 \\r\\n\\t]*([0-9]+)[\\x00 \\r\\n\\t]*obj[\\x00 \\r\\n\\t]*(.*)(endobj)?[\\x00 \\r\\n\\t]*$#s', $objBlob, $matches)) { + list($dummy, $generation, $objectData) = $matches; + if (preg_match('#^<<[\r\n\s]*(/Type|/Pages|/Parent [0-9]+ [0-9]+ [A-Z]|/Count [0-9]+|/Kids *\\[[0-9A-Z ]+\\]|[\r\n\s])+[\r\n\s]*>>#', $objectData, $matches)) { + if (preg_match('#/Count ([0-9]+)#', $objectData, $matches)) { + $info['pdf']['pages'] = (int) $matches[1]; + break; // for now this is the only data we're looking for in the PDF not need to loop through every object in the file (and a large PDF may contain MANY objects). And it MAY be possible that there are other objects elsewhere in the file that define additional (or removed?) pages + } + } + } else { + $this->error('Unexpected structure "'.substr($objBlob, 0, 100).'" at offset '.$offset); + break; + } + } + } + if (!$this->returnXREF) { + unset($info['pdf']['xref']['offset'], $info['pdf']['xref']['generation'], $info['pdf']['xref']['entry'], $info['pdf']['xref']['xref_offsets']); + } + + } else { + $this->error('Did not find "xref" at offset '.$info['pdf']['trailer']['startxref']); + } + } else { + $this->error('Did not find "startxref" in the last 40 bytes of the PDF'); + } + + $this->warning('PDF parsing incomplete in this version of getID3() ['.$this->getid3->version().']'); + return true; + } + $this->error('Did not find "%PDF" at the beginning of the PDF'); return false; } + /** + * @return bool + */ + private function parseXREF($XREFoffset) { + $info = &$this->getid3->info; + + $this->fseek($XREFoffset); + if (rtrim($this->fgets()) == 'xref') { + + $info['pdf']['xref']['xref_offsets'][$XREFoffset] = $XREFoffset; + list($firstObjectNumber, $XREFcount) = explode(' ', rtrim($this->fgets())); + $firstObjectNumber = (int) $firstObjectNumber; + $XREFcount = (int) $XREFcount; + $info['pdf']['xref']['count'] = $XREFcount + (!empty($info['pdf']['xref']['count']) ? $info['pdf']['xref']['count'] : 0); + for ($i = 0; $i < $XREFcount; $i++) { + $line = rtrim($this->fgets()); + if (preg_match('#^([0-9]+) ([0-9]+) ([nf])$#', $line, $matches)) { + $info['pdf']['xref']['offset'][($firstObjectNumber + $i)] = (int) $matches[1]; + $info['pdf']['xref']['generation'][($firstObjectNumber + $i)] = (int) $matches[2]; + $info['pdf']['xref']['entry'][($firstObjectNumber + $i)] = $matches[3]; + } else { + $this->error('failed to parse XREF entry #'.$i.' in XREF table at offset '.$XREFoffset); + return false; + } + } + sort($info['pdf']['xref']['xref_offsets']); + return true; + + } + $this->warning('failed to find expected XREF structure at offset '.$XREFoffset); + return false; + } + } diff --git a/lib/getid3/module.misc.torrent.php b/lib/getid3/module.misc.torrent.php new file mode 100644 index 0000000..1e25f95 --- /dev/null +++ b/lib/getid3/module.misc.torrent.php @@ -0,0 +1,247 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.misc.torrent.php // +// module for analyzing .torrent files // +// dependencies: NONE // +// /// +///////////////////////////////////////////////////////////////// + +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + +class getid3_torrent extends getid3_handler +{ + /** + * Assume all .torrent files are less than 1MB and just read entire thing into memory for easy processing. + * Override this value if you need to process files larger than 1MB + * + * @var int + */ + public $max_torrent_filesize = 1048576; + + /** + * calculated InfoHash (SHA1 of the entire "info" Dictionary) + * + * @var string + */ + private $infohash = ''; + + const PIECE_HASHLENGTH = 20; // number of bytes the SHA1 hash is for each piece + + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + $filesize = $info['avdataend'] - $info['avdataoffset']; + if ($filesize > $this->max_torrent_filesize) { // + $this->error('File larger ('.number_format($filesize).' bytes) than $max_torrent_filesize ('.number_format($this->max_torrent_filesize).' bytes), increase getid3_torrent->max_torrent_filesize if needed'); + return false; + } + $this->fseek($info['avdataoffset']); + $TORRENT = $this->fread($filesize); + $offset = 0; + if (!preg_match('#^(d8\\:announce|d7\\:comment)#', $TORRENT)) { + $this->error('Expecting "d8:announce" or "d7:comment" at '.$info['avdataoffset'].', found "'.substr($TORRENT, $offset, 12).'" instead.'); + return false; + } + $info['fileformat'] = 'torrent'; + + $info['torrent'] = $this->NextEntity($TORRENT, $offset); + if ($this->infohash) { + $info['torrent']['infohash'] = $this->infohash; + } + + if (empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['files'][0]['length'])) { + $info['torrent']['info']['length'] = 0; + foreach ($info['torrent']['info']['files'] as $key => $filedetails) { + $info['torrent']['info']['length'] += $filedetails['length']; + } + } + if (!empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['piece length']) && !empty($info['torrent']['info']['pieces'])) { + $num_pieces_size = ceil($info['torrent']['info']['length'] / $info['torrent']['info']['piece length']); + $num_pieces_hash = strlen($info['torrent']['info']['pieces']) / getid3_torrent::PIECE_HASHLENGTH; // should be concatenated 20-byte SHA1 hashes + if ($num_pieces_hash == $num_pieces_size) { + $info['torrent']['info']['piece_hash'] = array(); + for ($i = 0; $i < $num_pieces_size; $i++) { + $info['torrent']['info']['piece_hash'][$i] = ''; + for ($j = 0; $j < getid3_torrent::PIECE_HASHLENGTH; $j++) { + $info['torrent']['info']['piece_hash'][$i] .= sprintf('%02x', ord($info['torrent']['info']['pieces'][(($i * getid3_torrent::PIECE_HASHLENGTH) + $j)])); + } + } + unset($info['torrent']['info']['pieces']); + } else { + $this->warning('found '.$num_pieces_size.' pieces based on file/chunk size; found '.$num_pieces_hash.' pieces in hash table'); + } + } + if (!empty($info['torrent']['info']['name']) && !empty($info['torrent']['info']['length']) && !isset($info['torrent']['info']['files'])) { + // single-file torrent + $info['torrent']['files'] = array($info['torrent']['info']['name'] => $info['torrent']['info']['length']); + } elseif (!empty($info['torrent']['info']['files'])) { + // multi-file torrent + $info['torrent']['files'] = array(); + foreach ($info['torrent']['info']['files'] as $key => $filedetails) { + $info['torrent']['files'][implode('/', $filedetails['path'])] = $filedetails['length']; + } + } else { + $this->warning('no files found'); + } + + return true; + } + + /** + * @return string|array|int|bool + */ + public function NextEntity(&$TORRENT, &$offset) { + // https://fileformats.fandom.com/wiki/Torrent_file + // https://en.wikipedia.org/wiki/Torrent_file + // https://en.wikipedia.org/wiki/Bencode + + if ($offset >= strlen($TORRENT)) { + $this->error('cannot read beyond end of file '.$offset); + return false; + } + $type = $TORRENT[$offset++]; + if ($type == 'i') { + + // Integers are stored as ie: + // i90e + $value = $this->ReadSequentialDigits($TORRENT, $offset, true); + if ($TORRENT[$offset++] == 'e') { +//echo '
  • int: '.$value.'
  • '; + return (int) $value; + } + $this->error('unexpected('.__LINE__.') input "'.$value.'" at offset '.($offset - 1)); + return false; + + } elseif ($type == 'd') { + + // Dictionaries are stored as d[key1][value1][key2][value2][...]e. Keys and values appear alternately. + // Keys must be strings and must be ordered alphabetically. + // For example, {apple-red, lemon-yellow, violet-blue, banana-yellow} is stored as: + // d5:apple3:red6:banana6:yellow5:lemon6:yellow6:violet4:bluee + $values = array(); +//echo 'DICTIONARY @ '.$offset.'
      '; + $info_dictionary_start = null; // dummy declaration to prevent "Variable might not be defined" warnings + while (true) { + if ($TORRENT[$offset] === 'e') { + break; + } + $thisentry = array(); + $key = $this->NextEntity($TORRENT, $offset); + if ($key == 'info') { + $info_dictionary_start = $offset; + } + if ($key === false) { + $this->error('unexpected('.__LINE__.') input at offset '.$offset); + return false; + } + $value = $this->NextEntity($TORRENT, $offset); + if ($key == 'info') { + $info_dictionary_end = $offset; + $this->infohash = sha1(substr($TORRENT, $info_dictionary_start, $info_dictionary_end - $info_dictionary_start)); + } + if ($value === false) { + $this->error('unexpected('.__LINE__.') input at offset '.$offset); + return false; + } + $values[$key] = $value; + } + if ($TORRENT[$offset++] == 'e') { +//echo '
    '; + return $values; + } + $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1)); + return false; + + } elseif ($type == 'l') { + +//echo 'LIST @ '.$offset.'
      '; + // Lists are stored as l[value 1][value2][value3][...]e. For example, {spam, eggs, cheeseburger} is stored as: + // l4:spam4:eggs12:cheeseburgere + $values = array(); + while (true) { + if ($TORRENT[$offset] === 'e') { + break; + } + $NextEntity = $this->NextEntity($TORRENT, $offset); + if ($NextEntity === false) { + $this->error('unexpected('.__LINE__.') input at offset '.($offset - 1)); + return false; + } + $values[] = $NextEntity; + } + if ($TORRENT[$offset++] == 'e') { +//echo '
    '; + return $values; + } + $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1)); + return false; + + } elseif (ctype_digit($type)) { + + // Strings are stored as :: + // 4:wiki + $length = $type; + while (true) { + $char = $TORRENT[$offset++]; + if ($char == ':') { + break; + } elseif (!ctype_digit($char)) { + $this->error('unexpected('.__LINE__.') input "'.$char.'" at offset '.($offset - 1)); + return false; + } + $length .= $char; + } + if (($offset + $length) > strlen($TORRENT)) { + $this->error('string at offset '.$offset.' claims to be '.$length.' bytes long but only '.(strlen($TORRENT) - $offset).' bytes of data left in file'); + return false; + } + $string = substr($TORRENT, $offset, $length); + $offset += $length; +//echo '
  • string: '.$string.'
  • '; + return (string) $string; + + } else { + + $this->error('unexpected('.__LINE__.') input "'.$type.'" at offset '.($offset - 1)); + return false; + + } + } + + /** + * @return string + */ + public function ReadSequentialDigits(&$TORRENT, &$offset, $allow_negative=false) { + $start_offset = $offset; + $value = ''; + while (true) { + $char = $TORRENT[$offset++]; + if (!ctype_digit($char)) { + if ($allow_negative && ($char == '-') && (strlen($value) == 0)) { + // allow negative-sign if first character and $allow_negative enabled + } else { + $offset--; + break; + } + } + $value .= $char; + } + if (($value[0] === '0') && ($value !== '0')) { + $this->warning('illegal zero-padded number "'.$value.'" at offset '.$start_offset); + } + return $value; + } + +} diff --git a/lib/getid3/module.tag.apetag.php b/lib/getid3/module.tag.apetag.php index eb081b4..1305cfb 100644 --- a/lib/getid3/module.tag.apetag.php +++ b/lib/getid3/module.tag.apetag.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.tag.apetag.php // @@ -14,11 +14,27 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} + class getid3_apetag extends getid3_handler { - public $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory + /** + * true: return full data for all attachments; + * false: return no data for all attachments; + * integer: return data for attachments <= than this; + * string: save as file to this directory. + * + * @var int|bool|string + */ + public $inline_attachments = true; + public $overrideendoffset = 0; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -150,8 +166,8 @@ class getid3_apetag extends getid3_handler switch (strtolower($item_key)) { // http://wiki.hydrogenaud.io/index.php?title=ReplayGain#MP3Gain case 'replaygain_track_gain': - if (preg_match('#^[\\-\\+][0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { - $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! + if (preg_match('#^([\\-\\+][0-9\\.,]{8})( dB)?$#', $thisfile_ape_items_current['data'][0], $matches)) { + $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! $thisfile_replaygain['track']['originator'] = 'unspecified'; } else { $this->warning('MP3gainTrackGain value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); @@ -159,8 +175,8 @@ class getid3_apetag extends getid3_handler break; case 'replaygain_track_peak': - if (preg_match('#^[0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { - $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! + if (preg_match('#^([0-9\\.,]{8})$#', $thisfile_ape_items_current['data'][0], $matches)) { + $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! $thisfile_replaygain['track']['originator'] = 'unspecified'; if ($thisfile_replaygain['track']['peak'] <= 0) { $this->warning('ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'); @@ -171,8 +187,8 @@ class getid3_apetag extends getid3_handler break; case 'replaygain_album_gain': - if (preg_match('#^[\\-\\+][0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { - $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! + if (preg_match('#^([\\-\\+][0-9\\.,]{8})( dB)?$#', $thisfile_ape_items_current['data'][0], $matches)) { + $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! $thisfile_replaygain['album']['originator'] = 'unspecified'; } else { $this->warning('MP3gainAlbumGain value in APEtag appears invalid: "'.$thisfile_ape_items_current['data'][0].'"'); @@ -180,8 +196,8 @@ class getid3_apetag extends getid3_handler break; case 'replaygain_album_peak': - if (preg_match('#^[0-9\\.,]{8}$#', $thisfile_ape_items_current['data'][0])) { - $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! + if (preg_match('#^([0-9\\.,]{8})$#', $thisfile_ape_items_current['data'][0], $matches)) { + $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $matches[1]); // float casting will see "0,95" as zero! $thisfile_replaygain['album']['originator'] = 'unspecified'; if ($thisfile_replaygain['album']['peak'] <= 0) { $this->warning('ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'); @@ -225,7 +241,7 @@ class getid3_apetag extends getid3_handler case 'tracknumber': if (is_array($thisfile_ape_items_current['data'])) { foreach ($thisfile_ape_items_current['data'] as $comment) { - $thisfile_ape['comments']['track'][] = $comment; + $thisfile_ape['comments']['track_number'][] = $comment; } } break; @@ -251,7 +267,7 @@ class getid3_apetag extends getid3_handler case 'cover art (publisher logo)': case 'cover art (recording)': case 'cover art (studio)': - // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html + // list of possible cover arts from https://github.com/mono/taglib-sharp/blob/taglib-sharp-2.0.3.2/src/TagLib/Ape/Tag.cs if (is_array($thisfile_ape_items_current['data'])) { $this->warning('APEtag "'.$item_key.'" should be flagged as Binary data, but was incorrectly flagged as UTF-8'); $thisfile_ape_items_current['data'] = implode("\x00", $thisfile_ape_items_current['data']); @@ -316,7 +332,7 @@ class getid3_apetag extends getid3_handler $info['ape']['comments']['picture'][] = $comments_picture_data; unset($comments_picture_data); } - } while (false); + } while (false); // @phpstan-ignore-line break; default: @@ -335,10 +351,16 @@ class getid3_apetag extends getid3_handler return true; } + /** + * @param string $APEheaderFooterData + * + * @return array|false + */ public function parseAPEheaderFooter($APEheaderFooterData) { // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html // shortcut + $headerfooterinfo = array(); $headerfooterinfo['raw'] = array(); $headerfooterinfo_raw = &$headerfooterinfo['raw']; @@ -359,10 +381,16 @@ class getid3_apetag extends getid3_handler return $headerfooterinfo; } + /** + * @param int $rawflagint + * + * @return array + */ public function parseAPEtagFlags($rawflagint) { // "Note: APE Tags 1.0 do not use any of the APE Tag flags. // All are set to zero on creation and ignored on reading." // http://wiki.hydrogenaud.io/index.php?title=Ape_Tags_Flags + $flags = array(); $flags['header'] = (bool) ($rawflagint & 0x80000000); $flags['footer'] = (bool) ($rawflagint & 0x40000000); $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000); @@ -374,6 +402,11 @@ class getid3_apetag extends getid3_handler return $flags; } + /** + * @param int $contenttypeid + * + * @return string + */ public function APEcontentTypeFlagLookup($contenttypeid) { static $APEcontentTypeFlagLookup = array( 0 => 'utf-8', @@ -384,6 +417,11 @@ class getid3_apetag extends getid3_handler return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid'); } + /** + * @param string $itemkey + * + * @return bool + */ public function APEtagItemIsUTF8Lookup($itemkey) { static $APEtagItemIsUTF8Lookup = array( 'title', diff --git a/lib/getid3/module.tag.id3v1.php b/lib/getid3/module.tag.id3v1.php index d160e9b..442aefe 100644 --- a/lib/getid3/module.tag.id3v1.php +++ b/lib/getid3/module.tag.id3v1.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.tag.id3v1.php // @@ -14,10 +14,15 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_id3v1 extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -26,14 +31,22 @@ class getid3_id3v1 extends getid3_handler return false; } - $this->fseek(-256, SEEK_END); - $preid3v1 = $this->fread(128); - $id3v1tag = $this->fread(128); + if($info['filesize'] < 256) { + $this->fseek(-128, SEEK_END); + $preid3v1 = ''; + $id3v1tag = $this->fread(128); + } else { + $this->fseek(-256, SEEK_END); + $preid3v1 = $this->fread(128); + $id3v1tag = $this->fread(128); + } + if (substr($id3v1tag, 0, 3) == 'TAG') { $info['avdataend'] = $info['filesize'] - 128; + $ParsedID3v1 = array(); $ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30)); $ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30)); $ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30)); @@ -43,9 +56,9 @@ class getid3_id3v1 extends getid3_handler // If second-last byte of comment field is null and last byte of comment field is non-null // then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number - if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) { - $ParsedID3v1['track'] = ord(substr($ParsedID3v1['comment'], 29, 1)); - $ParsedID3v1['comment'] = substr($ParsedID3v1['comment'], 0, 28); + if (($id3v1tag[125] === "\x00") && ($id3v1tag[126] !== "\x00")) { + $ParsedID3v1['track_number'] = ord(substr($ParsedID3v1['comment'], 29, 1)); + $ParsedID3v1['comment'] = substr($ParsedID3v1['comment'], 0, 28); } $ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']); @@ -53,33 +66,37 @@ class getid3_id3v1 extends getid3_handler if (!empty($ParsedID3v1['genre'])) { unset($ParsedID3v1['genreid']); } - if (isset($ParsedID3v1['genre']) && (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown'))) { + if (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown')) { unset($ParsedID3v1['genre']); } foreach ($ParsedID3v1 as $key => $value) { $ParsedID3v1['comments'][$key][0] = $value; } - // ID3v1 encoding detection hack START - // ID3v1 is defined as always using ISO-8859-1 encoding, but it is not uncommon to find files tagged with ID3v1 using Windows-1251 or other character sets - // Since ID3v1 has no concept of character sets there is no certain way to know we have the correct non-ISO-8859-1 character set, but we can guess - $ID3v1encoding = 'ISO-8859-1'; - foreach ($ParsedID3v1['comments'] as $tag_key => $valuearray) { - foreach ($valuearray as $key => $value) { - if (preg_match('#^[\\x00-\\x40\\xA8\\B8\\x80-\\xFF]+$#', $value)) { - foreach (array('Windows-1251', 'KOI8-R') as $id3v1_bad_encoding) { - if (function_exists('mb_convert_encoding') && @mb_convert_encoding($value, $id3v1_bad_encoding, $id3v1_bad_encoding) === $value) { - $ID3v1encoding = $id3v1_bad_encoding; - break 3; - } elseif (function_exists('iconv') && @iconv($id3v1_bad_encoding, $id3v1_bad_encoding, $value) === $value) { - $ID3v1encoding = $id3v1_bad_encoding; - break 3; + $ID3v1encoding = $this->getid3->encoding_id3v1; + if ($this->getid3->encoding_id3v1_autodetect) { + // ID3v1 encoding detection hack START + // ID3v1 is defined as always using ISO-8859-1 encoding, but it is not uncommon to find files tagged with ID3v1 using Windows-1251 or other character sets + // Since ID3v1 has no concept of character sets there is no certain way to know we have the correct non-ISO-8859-1 character set, but we can guess + foreach ($ParsedID3v1['comments'] as $tag_key => $valuearray) { + foreach ($valuearray as $key => $value) { + if (preg_match('#^[\\x00-\\x40\\x80-\\xFF]+$#', $value) && !ctype_digit((string) $value)) { // check for strings with only characters above chr(128) and punctuation/numbers, but not just numeric strings (e.g. track numbers or years) + foreach (array('Windows-1251', 'KOI8-R') as $id3v1_bad_encoding) { + if (function_exists('mb_convert_encoding') && @mb_convert_encoding($value, $id3v1_bad_encoding, $id3v1_bad_encoding) === $value) { + $ID3v1encoding = $id3v1_bad_encoding; + $this->warning('ID3v1 detected as '.$id3v1_bad_encoding.' text encoding in '.$tag_key); + break 3; + } elseif (function_exists('iconv') && @iconv($id3v1_bad_encoding, $id3v1_bad_encoding, $value) === $value) { + $ID3v1encoding = $id3v1_bad_encoding; + $this->warning('ID3v1 detected as '.$id3v1_bad_encoding.' text encoding in '.$tag_key); + break 3; + } } } } } + // ID3v1 encoding detection hack END } - // ID3v1 encoding detection hack END // ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces $GoodFormatID3v1tag = $this->GenerateID3v1Tag( @@ -89,7 +106,7 @@ class getid3_id3v1 extends getid3_handler $ParsedID3v1['year'], (isset($ParsedID3v1['genre']) ? $this->LookupGenreID($ParsedID3v1['genre']) : false), $ParsedID3v1['comment'], - (!empty($ParsedID3v1['track']) ? $ParsedID3v1['track'] : '')); + (!empty($ParsedID3v1['track_number']) ? $ParsedID3v1['track_number'] : '')); $ParsedID3v1['padding_valid'] = true; if ($id3v1tag !== $GoodFormatID3v1tag) { $ParsedID3v1['padding_valid'] = false; @@ -124,10 +141,20 @@ class getid3_id3v1 extends getid3_handler return true; } + /** + * @param string $str + * + * @return string + */ public static function cutfield($str) { return trim(substr($str, 0, strcspn($str, "\x00"))); } + /** + * @param bool $allowSCMPXextended + * + * @return string[] + */ public static function ArrayOfGenres($allowSCMPXextended=false) { static $GenreLookup = array( 0 => 'Blues', @@ -278,6 +305,50 @@ class getid3_id3v1 extends getid3_handler 145 => 'Anime', 146 => 'JPop', 147 => 'Synthpop', + 148 => 'Abstract', + 149 => 'Art Rock', + 150 => 'Baroque', + 151 => 'Bhangra', + 152 => 'Big Beat', + 153 => 'Breakbeat', + 154 => 'Chillout', + 155 => 'Downtempo', + 156 => 'Dub', + 157 => 'EBM', + 158 => 'Eclectic', + 159 => 'Electro', + 160 => 'Electroclash', + 161 => 'Emo', + 162 => 'Experimental', + 163 => 'Garage', + 164 => 'Global', + 165 => 'IDM', + 166 => 'Illbient', + 167 => 'Industro-Goth', + 168 => 'Jam Band', + 169 => 'Krautrock', + 170 => 'Leftfield', + 171 => 'Lounge', + 172 => 'Math Rock', + 173 => 'New Romantic', + 174 => 'Nu-Breakz', + 175 => 'Post-Punk', + 176 => 'Post-Rock', + 177 => 'Psytrance', + 178 => 'Shoegaze', + 179 => 'Space Rock', + 180 => 'Trop Rock', + 181 => 'World Music', + 182 => 'Neoclassical', + 183 => 'Audiobook', + 184 => 'Audio Theatre', + 185 => 'Neue Deutsche Welle', + 186 => 'Podcast', + 187 => 'Indie-Rock', + 188 => 'G-Funk', + 189 => 'Dubstep', + 190 => 'Garage Rock', + 191 => 'Psybient', 255 => 'Unknown', @@ -312,6 +383,12 @@ class getid3_id3v1 extends getid3_handler return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup); } + /** + * @param string $genreid + * @param bool $allowSCMPXextended + * + * @return string|false + */ public static function LookupGenreName($genreid, $allowSCMPXextended=true) { switch ($genreid) { case 'RX': @@ -328,6 +405,12 @@ class getid3_id3v1 extends getid3_handler return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false); } + /** + * @param string $genre + * @param bool $allowSCMPXextended + * + * @return string|false + */ public static function LookupGenreID($genre, $allowSCMPXextended=false) { $GenreLookup = self::ArrayOfGenres($allowSCMPXextended); $LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre)); @@ -339,6 +422,11 @@ class getid3_id3v1 extends getid3_handler return false; } + /** + * @param string $OriginalGenre + * + * @return string|false + */ public static function StandardiseID3v1GenreName($OriginalGenre) { if (($GenreID = self::LookupGenreID($OriginalGenre)) !== false) { return self::LookupGenreName($GenreID); @@ -346,6 +434,17 @@ class getid3_id3v1 extends getid3_handler return $OriginalGenre; } + /** + * @param string $title + * @param string $artist + * @param string $album + * @param string $year + * @param int $genreid + * @param string $comment + * @param int|string $track + * + * @return string + */ public static function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') { $ID3v1Tag = 'TAG'; $ID3v1Tag .= str_pad(trim(substr($title, 0, 30)), 30, "\x00", STR_PAD_RIGHT); diff --git a/lib/getid3/module.tag.id3v2.php b/lib/getid3/module.tag.id3v2.php index 139238a..5b90743 100644 --- a/lib/getid3/module.tag.id3v2.php +++ b/lib/getid3/module.tag.id3v2.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// /// // // module.tag.id3v2.php // @@ -14,12 +14,18 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v1.php', __FILE__, true); class getid3_id3v2 extends getid3_handler { public $StartingOffset = 0; + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -56,8 +62,8 @@ class getid3_id3v2 extends getid3_handler $header = $this->fread(10); if (substr($header, 0, 3) == 'ID3' && strlen($header) == 10) { - $thisfile_id3v2['majorversion'] = ord($header{3}); - $thisfile_id3v2['minorversion'] = ord($header{4}); + $thisfile_id3v2['majorversion'] = ord($header[3]); + $thisfile_id3v2['minorversion'] = ord($header[4]); // shortcut $id3v2_majorversion = &$thisfile_id3v2['majorversion']; @@ -76,7 +82,7 @@ class getid3_id3v2 extends getid3_handler } - $id3_flags = ord($header{5}); + $id3_flags = ord($header[5]); switch ($id3v2_majorversion) { case 2: // %ab000000 in v2.2 @@ -257,7 +263,7 @@ class getid3_id3v2 extends getid3_handler $thisfile_id3v2['padding']['length'] = strlen($framedata); $thisfile_id3v2['padding']['valid'] = true; for ($i = 0; $i < $thisfile_id3v2['padding']['length']; $i++) { - if ($framedata{$i} != "\x00") { + if ($framedata[$i] != "\x00") { $thisfile_id3v2['padding']['valid'] = false; $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i; $this->warning('Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)'); @@ -266,6 +272,10 @@ class getid3_id3v2 extends getid3_handler } break; // skip rest of ID3v2 header } + $frame_header = null; + $frame_name = null; + $frame_size = null; + $frame_flags = null; if ($id3v2_majorversion == 2) { // Frame ID $xx xx xx (three characters) // Size $xx xx xx (24-bit integer) @@ -319,7 +329,7 @@ class getid3_id3v2 extends getid3_handler $len = strlen($framedata); for ($i = 0; $i < $len; $i++) { - if ($framedata{$i} != "\x00") { + if ($framedata[$i] != "\x00") { $thisfile_id3v2['padding']['valid'] = false; $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i; $this->warning('Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)'); @@ -335,7 +345,7 @@ class getid3_id3v2 extends getid3_handler } if (($frame_size <= strlen($framedata)) && ($this->IsValidID3v2FrameName($frame_name, $id3v2_majorversion))) { - unset($parsedFrame); + $parsedFrame = array(); $parsedFrame['frame_name'] = $frame_name; $parsedFrame['frame_flags_raw'] = $frame_flags; $parsedFrame['data'] = substr($framedata, 0, $frame_size); @@ -427,11 +437,11 @@ class getid3_id3v2 extends getid3_handler $footer = $this->fread(10); if (substr($footer, 0, 3) == '3DI') { $thisfile_id3v2['footer'] = true; - $thisfile_id3v2['majorversion_footer'] = ord($footer{3}); - $thisfile_id3v2['minorversion_footer'] = ord($footer{4}); + $thisfile_id3v2['majorversion_footer'] = ord($footer[3]); + $thisfile_id3v2['minorversion_footer'] = ord($footer[4]); } if ($thisfile_id3v2['majorversion_footer'] <= 4) { - $id3_flags = ord(substr($footer{5})); + $id3_flags = ord($footer[5]); $thisfile_id3v2_flags['unsynch_footer'] = (bool) ($id3_flags & 0x80); $thisfile_id3v2_flags['extfoot_footer'] = (bool) ($id3_flags & 0x40); $thisfile_id3v2_flags['experim_footer'] = (bool) ($id3_flags & 0x20); @@ -452,10 +462,10 @@ class getid3_id3v2 extends getid3_handler unset($key, $value, $genres, $genre); } - if (isset($thisfile_id3v2['comments']['track'])) { - foreach ($thisfile_id3v2['comments']['track'] as $key => $value) { + if (isset($thisfile_id3v2['comments']['track_number'])) { + foreach ($thisfile_id3v2['comments']['track_number'] as $key => $value) { if (strstr($value, '/')) { - list($thisfile_id3v2['comments']['tracknum'][$key], $thisfile_id3v2['comments']['totaltracks'][$key]) = explode('/', $thisfile_id3v2['comments']['track'][$key]); + list($thisfile_id3v2['comments']['track_number'][$key], $thisfile_id3v2['comments']['totaltracks'][$key]) = explode('/', $thisfile_id3v2['comments']['track_number'][$key]); } } } @@ -498,7 +508,11 @@ class getid3_id3v2 extends getid3_handler return true; } - + /** + * @param string $genrestring + * + * @return array + */ public function ParseID3v2GenreString($genrestring) { // Parse genres into arrays of genreName and genreID // ID3v2.2.x, ID3v2.3.x: '(21)' or '(4)Eurodisco' or '(51)(39)' or '(55)((I think...)' @@ -509,14 +523,21 @@ class getid3_id3v2 extends getid3_handler if (($this->getid3->info['id3v2']['majorversion'] == 3) && !preg_match('#[\x00]#', $genrestring)) { // note: MusicBrainz Picard incorrectly stores plaintext genres separated by "/" when writing in ID3v2.3 mode, hack-fix here: // replace / with NULL, then replace back the two ID3v1 genres that legitimately have "/" as part of the single genre name - if (preg_match('#/#', $genrestring)) { + if (strpos($genrestring, '/') !== false) { + $LegitimateSlashedGenreList = array( // https://github.com/JamesHeinrich/getID3/issues/223 + 'Pop/Funk', // ID3v1 genre #62 - https://en.wikipedia.org/wiki/ID3#standard + 'Cut-up/DJ', // Discogs - https://www.discogs.com/style/cut-up/dj + 'RnB/Swing', // Discogs - https://www.discogs.com/style/rnb/swing + 'Funk / Soul', // Discogs (note spaces) - https://www.discogs.com/genre/funk+%2F+soul + ); $genrestring = str_replace('/', "\x00", $genrestring); - $genrestring = str_replace('Pop'."\x00".'Funk', 'Pop/Funk', $genrestring); - $genrestring = str_replace('Rock'."\x00".'Rock', 'Folk/Rock', $genrestring); + foreach ($LegitimateSlashedGenreList as $SlashedGenre) { + $genrestring = str_ireplace(str_replace('/', "\x00", $SlashedGenre), $SlashedGenre, $genrestring); + } } // some other taggers separate multiple genres with semicolon, e.g. "Heavy Metal;Thrash Metal;Metal" - if (preg_match('#;#', $genrestring)) { + if (strpos($genrestring, ';') !== false) { $genrestring = str_replace(';', "\x00", $genrestring); } } @@ -530,7 +551,7 @@ class getid3_id3v2 extends getid3_handler foreach ($genre_elements as $element) { $element = trim($element); if ($element) { - if (preg_match('#^[0-9]{1,3}#', $element)) { + if (preg_match('#^[0-9]{1,3}$#', $element)) { $clean_genres[] = getid3_id3v1::LookupGenreName($element); } else { $clean_genres[] = str_replace('((', '(', $element); @@ -540,7 +561,11 @@ class getid3_id3v2 extends getid3_handler return $clean_genres; } - + /** + * @param array $parsedFrame + * + * @return bool + */ public function ParseID3v2Frame(&$parsedFrame) { // shortcuts @@ -657,16 +682,14 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); $parsedFrame['encodingid'] = $frame_textencoding; $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - $parsedFrame['description'] = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $frame_description)); + $parsedFrame['description'] = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['description'])); $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); + $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $frame_textencoding_terminator); if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { $commentkey = ($parsedFrame['description'] ? $parsedFrame['description'] : (isset($info['id3v2']['comments'][$parsedFrame['framenameshort']]) ? count($info['id3v2']['comments'][$parsedFrame['framenameshort']]) : 0)); if (!isset($info['id3v2']['comments'][$parsedFrame['framenameshort']]) || !array_key_exists($commentkey, $info['id3v2']['comments'][$parsedFrame['framenameshort']])) { @@ -678,7 +701,7 @@ class getid3_id3v2 extends getid3_handler //unset($parsedFrame['data']); do not unset, may be needed elsewhere, e.g. for replaygain - } elseif ($parsedFrame['frame_name']{0} == 'T') { // 4.2. T??[?] Text information frame + } elseif ($parsedFrame['frame_name'][0] == 'T') { // 4.2. T??[?] Text information frame // There may only be one text information frame of its kind in an tag. //
    @@ -692,10 +715,10 @@ class getid3_id3v2 extends getid3_handler } $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); + $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding)); $parsedFrame['encodingid'] = $frame_textencoding; $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { // ID3v2.3 specs say that TPE1 (and others) can contain multiple artist values separated with / // This of course breaks when an artist name contains slash character, e.g. "AC/DC" @@ -751,38 +774,20 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); - - $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - if ($frame_terminatorpos) { - // there are null bytes after the data - this is not according to spec - // only use data up to first null byte - $frame_urldata = (string) substr($parsedFrame['data'], 0, $frame_terminatorpos); - } else { - // no null bytes following data, just use all data - $frame_urldata = (string) $parsedFrame['data']; - } - $parsedFrame['encodingid'] = $frame_textencoding; $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); // according to the frame text encoding + $parsedFrame['url'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); // always ISO-8859-1 + $parsedFrame['description'] = $this->RemoveStringTerminator($parsedFrame['description'], $frame_textencoding_terminator); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); - $parsedFrame['url'] = $frame_urldata; // always ISO-8859-1 - $parsedFrame['description'] = $frame_description; // according to the frame text encoding if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) { $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback('ISO-8859-1', $info['id3v2']['encoding'], $parsedFrame['url']); } unset($parsedFrame['data']); - } elseif ($parsedFrame['frame_name']{0} == 'W') { // 4.3. W??? URL link frames + } elseif ($parsedFrame['frame_name'][0] == 'W') { // 4.3. W??? URL link frames // There may only be one URL link frame of its kind in a tag, // except when stated otherwise in the frame description //
    TextEncodingNameLookup($parsedFrame['encodingid']); $parsedFrame['data_raw'] = (string) substr($parsedFrame['data'], $frame_offset); - // http://www.getid3.org/phpBB3/viewtopic.php?t=1369 + // https://www.getid3.org/phpBB3/viewtopic.php?t=1369 // "this tag typically contains null terminated strings, which are associated in pairs" // "there are users that use the tag incorrectly" $IPLS_parts = array(); @@ -933,6 +938,7 @@ class getid3_id3v2 extends getid3_handler $parsedFrame['bitsforbytesdeviation'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 8, 1)); $parsedFrame['bitsformsdeviation'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 9, 1)); $parsedFrame['data'] = substr($parsedFrame['data'], 10); + $deviationbitstream = ''; while ($frame_offset < strlen($parsedFrame['data'])) { $deviationbitstream .= getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1)); } @@ -972,7 +978,7 @@ class getid3_id3v2 extends getid3_handler } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'USLT')) || // 4.8 USLT Unsynchronised lyric/text transcription - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ULT'))) { // 4.9 ULT Unsynchronised lyric/text transcription + (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ULT'))) { // 4.9 ULT Unsynchronised lyric/text transcription // There may be more than one 'Unsynchronised lyrics/text transcription' frame // in each tag, but only one with the same language and content descriptor. //
    @@ -988,27 +994,28 @@ class getid3_id3v2 extends getid3_handler $this->warning('Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'); $frame_textencoding_terminator = "\x00"; } - $frame_language = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); + if (strlen($parsedFrame['data']) >= (4 + strlen($frame_textencoding_terminator))) { // shouldn't be an issue but badly-written files have been spotted in the wild with not only no contents but also missing the required language field, see https://github.com/JamesHeinrich/getID3/issues/315 + $frame_language = substr($parsedFrame['data'], $frame_offset, 3); + $frame_offset += 3; + $frame_terminatorpos = strpos($parsedFrame['data'], $frame_textencoding_terminator, $frame_offset); + if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { + $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 + } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); + $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); + $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $frame_textencoding_terminator); - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); + $parsedFrame['encodingid'] = $frame_textencoding; + $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - $parsedFrame['language'] = $frame_language; - $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - $parsedFrame['description'] = $frame_description; - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); + $parsedFrame['language'] = $frame_language; + $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); + if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { + $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); + } + } else { + $this->warning('Invalid data in frame "'.$parsedFrame['frame_name'].'" at offset '.$parsedFrame['dataoffset']); } unset($parsedFrame['data']); @@ -1061,7 +1068,7 @@ class getid3_id3v2 extends getid3_handler $parsedFrame['lyrics'][$timestampindex]['data'] = substr($frame_remainingdata, $frame_offset, $frame_terminatorpos - $frame_offset); $frame_remainingdata = substr($frame_remainingdata, $frame_terminatorpos + strlen($frame_textencoding_terminator)); - if (($timestampindex == 0) && (ord($frame_remainingdata{0}) != 0)) { + if (($timestampindex == 0) && (ord($frame_remainingdata[0]) != 0)) { // timestamp probably omitted for first data item } else { $parsedFrame['lyrics'][$timestampindex]['timestamp'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 0, 4)); @@ -1102,19 +1109,16 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); $frame_text = (string) substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); + $frame_text = $this->RemoveStringTerminator($frame_text, $frame_textencoding_terminator); $parsedFrame['encodingid'] = $frame_textencoding; $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); $parsedFrame['language'] = $frame_language; $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - $parsedFrame['description'] = $frame_description; $parsedFrame['data'] = $frame_text; if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { $commentkey = ($parsedFrame['description'] ? $parsedFrame['description'] : (!empty($info['id3v2']['comments'][$parsedFrame['framenameshort']]) ? count($info['id3v2']['comments'][$parsedFrame['framenameshort']]) : 0)); @@ -1370,6 +1374,8 @@ class getid3_id3v2 extends getid3_handler $frame_textencoding_terminator = "\x00"; } + $frame_imagetype = null; + $frame_mimetype = null; if ($id3v2_majorversion == 2 && strlen($parsedFrame['data']) > $frame_offset) { $frame_imagetype = substr($parsedFrame['data'], $frame_offset, 3); if (strtolower($frame_imagetype) == 'ima') { @@ -1407,30 +1413,26 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); + $parsedFrame['encodingid'] = $frame_textencoding; + $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); if ($id3v2_majorversion == 2) { - $parsedFrame['imagetype'] = $frame_imagetype; + $parsedFrame['imagetype'] = isset($frame_imagetype) ? $frame_imagetype : null; } else { - $parsedFrame['mime'] = $frame_mimetype; + $parsedFrame['mime'] = isset($frame_mimetype) ? $frame_mimetype : null; } - $parsedFrame['picturetypeid'] = $frame_picturetype; - $parsedFrame['picturetype'] = $this->APICPictureTypeLookup($frame_picturetype); - $parsedFrame['description'] = $frame_description; - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); - $parsedFrame['datalength'] = strlen($parsedFrame['data']); + $parsedFrame['picturetypeid'] = $frame_picturetype; + $parsedFrame['picturetype'] = $this->APICPictureTypeLookup($frame_picturetype); + $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator)); + $parsedFrame['datalength'] = strlen($parsedFrame['data']); - $parsedFrame['image_mime'] = ''; + $parsedFrame['image_mime'] = ''; $imageinfo = array(); if ($imagechunkcheck = getid3_lib::GetDataImageSize($parsedFrame['data'], $imageinfo)) { if (($imagechunkcheck[2] >= 1) && ($imagechunkcheck[2] <= 3)) { - $parsedFrame['image_mime'] = 'image/'.getid3_lib::ImageTypesLookup($imagechunkcheck[2]); + $parsedFrame['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); if ($imagechunkcheck[0]) { $parsedFrame['image_width'] = $imagechunkcheck[0]; } @@ -1446,6 +1448,7 @@ class getid3_id3v2 extends getid3_handler unset($parsedFrame['data']); break; } + $dir = ''; if ($this->getid3->option_save_attachments === true) { // great /* @@ -1491,7 +1494,7 @@ class getid3_id3v2 extends getid3_handler unset($comments_picture_data); } } - } while (false); + } while (false); // @phpstan-ignore-line } } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GEOB')) || // 4.15 GEOB General encapsulated object @@ -1533,11 +1536,8 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator); $parsedFrame['objectdata'] = (string) substr($parsedFrame['data'], $frame_offset); @@ -1546,7 +1546,6 @@ class getid3_id3v2 extends getid3_handler $parsedFrame['mime'] = $frame_mimetype; $parsedFrame['filename'] = $frame_filename; - $parsedFrame['description'] = $frame_description; unset($parsedFrame['data']); @@ -1616,16 +1615,12 @@ class getid3_id3v2 extends getid3_handler $frame_offset = $frame_terminatorpos + strlen("\x00"); $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); $frame_offset = $frame_terminatorpos + strlen("\x00"); $parsedFrame['ownerid'] = $frame_ownerid; $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - $parsedFrame['description'] = $frame_description; unset($parsedFrame['data']); @@ -1721,7 +1716,8 @@ class getid3_id3v2 extends getid3_handler $parsedFrame['encodingid'] = $frame_textencoding; $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); + $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); + $parsedFrame['data'] = $this->RemoveStringTerminator($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding)); if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); } @@ -1759,6 +1755,7 @@ class getid3_id3v2 extends getid3_handler $frame_offset += 8; $parsedFrame['seller'] = (string) substr($parsedFrame['data'], $frame_offset); + $parsedFrame['seller'] = $this->RemoveStringTerminator($parsedFrame['seller'], $this->TextEncodingTerminatorLookup($frame_textencoding)); unset($parsedFrame['data']); @@ -1817,11 +1814,8 @@ class getid3_id3v2 extends getid3_handler if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($frame_textencoding_terminator), 1)) === 0) { $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (in_array($frame_description, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { - // if description only contains a BOM or terminator then make it blank - $frame_description = ''; - } + $parsedFrame['description'] = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); + $parsedFrame['description'] = $this->MakeUTF16emptyStringEmpty($parsedFrame['description']); $frame_offset = $frame_terminatorpos + strlen($frame_textencoding_terminator); $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); @@ -1838,7 +1832,6 @@ class getid3_id3v2 extends getid3_handler $parsedFrame['receivedasid'] = $frame_receivedasid; $parsedFrame['receivedas'] = $this->COMRReceivedAsLookup($frame_receivedasid); $parsedFrame['sellername'] = $frame_sellername; - $parsedFrame['description'] = $frame_description; $parsedFrame['mime'] = $frame_mimetype; $parsedFrame['logo'] = $frame_sellerlogo; unset($parsedFrame['data']); @@ -1969,18 +1962,14 @@ class getid3_id3v2 extends getid3_handler $frame_offset = 0; $parsedFrame['peakamplitude'] = getid3_lib::BigEndian2Float(substr($parsedFrame['data'], $frame_offset, 4)); $frame_offset += 4; - $rg_track_adjustment = getid3_lib::Dec2Bin(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $rg_album_adjustment = getid3_lib::Dec2Bin(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['raw']['track']['name'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 0, 3)); - $parsedFrame['raw']['track']['originator'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 3, 3)); - $parsedFrame['raw']['track']['signbit'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 6, 1)); - $parsedFrame['raw']['track']['adjustment'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 7, 9)); - $parsedFrame['raw']['album']['name'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 0, 3)); - $parsedFrame['raw']['album']['originator'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 3, 3)); - $parsedFrame['raw']['album']['signbit'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 6, 1)); - $parsedFrame['raw']['album']['adjustment'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 7, 9)); + foreach (array('track','album') as $rgad_entry_type) { + $rg_adjustment_word = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); + $frame_offset += 2; + $parsedFrame['raw'][$rgad_entry_type]['name'] = ($rg_adjustment_word & 0xE000) >> 13; + $parsedFrame['raw'][$rgad_entry_type]['originator'] = ($rg_adjustment_word & 0x1C00) >> 10; + $parsedFrame['raw'][$rgad_entry_type]['signbit'] = ($rg_adjustment_word & 0x0200) >> 9; + $parsedFrame['raw'][$rgad_entry_type]['adjustment'] = ($rg_adjustment_word & 0x0100); + } $parsedFrame['track']['name'] = getid3_lib::RGADnameLookup($parsedFrame['raw']['track']['name']); $parsedFrame['track']['originator'] = getid3_lib::RGADoriginatorLookup($parsedFrame['raw']['track']['originator']); $parsedFrame['track']['adjustment'] = getid3_lib::RGADadjustmentLookup($parsedFrame['raw']['track']['adjustment'], $parsedFrame['raw']['track']['signbit']); @@ -2002,9 +1991,9 @@ class getid3_id3v2 extends getid3_handler // Element ID $00 // Start time $xx xx xx xx // End time $xx xx xx xx - // Start offset $xx xx xx xx - // End offset $xx xx xx xx - // + // Start offset $xx xx xx xx + // End offset $xx xx xx xx + // $frame_offset = 0; @list($parsedFrame['element_id']) = explode("\x00", $parsedFrame['data'], 2); @@ -2045,7 +2034,7 @@ class getid3_id3v2 extends getid3_handler $subframe['encodingid'] = ord(substr($subframe_rawdata, 0, 1)); $subframe['text'] = substr($subframe_rawdata, 1); $subframe['encoding'] = $this->TextEncodingNameLookup($subframe['encodingid']); - $encoding_converted_text = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe['text']));; + $encoding_converted_text = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe['text'])); switch (substr($encoding_converted_text, 0, 2)) { case "\xFF\xFE": case "\xFE\xFF": @@ -2065,22 +2054,51 @@ class getid3_id3v2 extends getid3_handler break; } - if (($subframe['name'] == 'TIT2') || ($subframe['name'] == 'TIT3')) { - if ($subframe['name'] == 'TIT2') { + switch ($subframe['name']) { + case 'TIT2': $parsedFrame['chapter_name'] = $encoding_converted_text; - } elseif ($subframe['name'] == 'TIT3') { + $parsedFrame['subframes'][] = $subframe; + break; + case 'TIT3': $parsedFrame['chapter_description'] = $encoding_converted_text; - } - $parsedFrame['subframes'][] = $subframe; - } else { - $this->warning('ID3v2.CHAP subframe "'.$subframe['name'].'" not handled (only TIT2 and TIT3)'); + $parsedFrame['subframes'][] = $subframe; + break; + case 'WXXX': + @list($subframe['chapter_url_description'], $subframe['chapter_url']) = explode("\x00", $encoding_converted_text, 2); + $parsedFrame['chapter_url'][$subframe['chapter_url_description']] = $subframe['chapter_url']; + $parsedFrame['subframes'][] = $subframe; + break; + case 'APIC': + if (preg_match('#^([^\\x00]+)*\\x00(.)([^\\x00]+)*\\x00(.+)$#s', $subframe['text'], $matches)) { + list($dummy, $subframe_apic_mime, $subframe_apic_picturetype, $subframe_apic_description, $subframe_apic_picturedata) = $matches; + $subframe['image_mime'] = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe_apic_mime)); + $subframe['picture_type'] = $this->APICPictureTypeLookup($subframe_apic_picturetype); + $subframe['description'] = trim(getid3_lib::iconv_fallback($subframe['encoding'], $info['encoding'], $subframe_apic_description)); + if (strlen($this->TextEncodingTerminatorLookup($subframe['encoding'])) == 2) { + // the null terminator between "description" and "picture data" could be either 1 byte (ISO-8859-1, UTF-8) or two bytes (UTF-16) + // the above regex assumes one byte, if it's actually two then strip the second one here + $subframe_apic_picturedata = substr($subframe_apic_picturedata, 1); + } + $subframe['data'] = $subframe_apic_picturedata; + unset($dummy, $subframe_apic_mime, $subframe_apic_picturetype, $subframe_apic_description, $subframe_apic_picturedata); + unset($subframe['text'], $parsedFrame['text']); + $parsedFrame['subframes'][] = $subframe; + $parsedFrame['picture_present'] = true; + } else { + $this->warning('ID3v2.CHAP subframe #'.(count($parsedFrame['subframes']) + 1).' "'.$subframe['name'].'" not in expected format'); + } + break; + default: + $this->warning('ID3v2.CHAP subframe "'.$subframe['name'].'" not handled (supported: TIT2, TIT3, WXXX, APIC)'); + break; } } unset($subframe_rawdata, $subframe, $encoding_converted_text); + unset($parsedFrame['data']); // debatable whether this this be here, without it the returned structure may contain a large amount of duplicate data if chapters contain APIC } $id3v2_chapter_entry = array(); - foreach (array('id', 'time_begin', 'time_end', 'offset_begin', 'offset_end', 'chapter_name', 'chapter_description') as $id3v2_chapter_key) { + foreach (array('id', 'time_begin', 'time_end', 'offset_begin', 'offset_end', 'chapter_name', 'chapter_description', 'chapter_url', 'picture_present') as $id3v2_chapter_key) { if (isset($parsedFrame[$id3v2_chapter_key])) { $id3v2_chapter_entry[$id3v2_chapter_key] = $parsedFrame[$id3v2_chapter_key]; } @@ -2099,7 +2117,7 @@ class getid3_id3v2 extends getid3_handler // CTOC flags %xx // Entry count $xx // Child Element ID $00 /* zero or more child CHAP or CTOC entries */ - // + // $frame_offset = 0; @list($parsedFrame['element_id']) = explode("\x00", $parsedFrame['data'], 2); @@ -2181,11 +2199,20 @@ class getid3_id3v2 extends getid3_handler return true; } - + /** + * @param string $data + * + * @return string + */ public function DeUnsynchronise($data) { return str_replace("\xFF\x00", "\xFF", $data); } + /** + * @param int $index + * + * @return string + */ public function LookupExtendedHeaderRestrictionsTagSizeLimits($index) { static $LookupExtendedHeaderRestrictionsTagSizeLimits = array( 0x00 => 'No more than 128 frames and 1 MB total tag size', @@ -2196,6 +2223,11 @@ class getid3_id3v2 extends getid3_handler return (isset($LookupExtendedHeaderRestrictionsTagSizeLimits[$index]) ? $LookupExtendedHeaderRestrictionsTagSizeLimits[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public function LookupExtendedHeaderRestrictionsTextEncodings($index) { static $LookupExtendedHeaderRestrictionsTextEncodings = array( 0x00 => 'No restrictions', @@ -2204,6 +2236,11 @@ class getid3_id3v2 extends getid3_handler return (isset($LookupExtendedHeaderRestrictionsTextEncodings[$index]) ? $LookupExtendedHeaderRestrictionsTextEncodings[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public function LookupExtendedHeaderRestrictionsTextFieldSize($index) { static $LookupExtendedHeaderRestrictionsTextFieldSize = array( 0x00 => 'No restrictions', @@ -2214,6 +2251,11 @@ class getid3_id3v2 extends getid3_handler return (isset($LookupExtendedHeaderRestrictionsTextFieldSize[$index]) ? $LookupExtendedHeaderRestrictionsTextFieldSize[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public function LookupExtendedHeaderRestrictionsImageEncoding($index) { static $LookupExtendedHeaderRestrictionsImageEncoding = array( 0x00 => 'No restrictions', @@ -2222,6 +2264,11 @@ class getid3_id3v2 extends getid3_handler return (isset($LookupExtendedHeaderRestrictionsImageEncoding[$index]) ? $LookupExtendedHeaderRestrictionsImageEncoding[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public function LookupExtendedHeaderRestrictionsImageSizeSize($index) { static $LookupExtendedHeaderRestrictionsImageSizeSize = array( 0x00 => 'No restrictions', @@ -2232,6 +2279,11 @@ class getid3_id3v2 extends getid3_handler return (isset($LookupExtendedHeaderRestrictionsImageSizeSize[$index]) ? $LookupExtendedHeaderRestrictionsImageSizeSize[$index] : ''); } + /** + * @param string $currencyid + * + * @return string + */ public function LookupCurrencyUnits($currencyid) { $begin = __LINE__; @@ -2394,7 +2446,8 @@ class getid3_id3v2 extends getid3_handler TMM Manats TND Dinars TOP Pa'anga - TRL Liras + TRL Liras (old) + TRY Liras TTD Dollars TVD Tuvalu Dollars TWD New Dollars @@ -2428,7 +2481,11 @@ class getid3_id3v2 extends getid3_handler return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-units'); } - + /** + * @param string $currencyid + * + * @return string + */ public function LookupCurrencyCountry($currencyid) { $begin = __LINE__; @@ -2591,6 +2648,7 @@ class getid3_id3v2 extends getid3_handler TND Tunisia TOP Tonga TRL Turkey + TRY Turkey TTD Trinidad and Tobago TVD Tuvalu TWD Taiwan @@ -2624,8 +2682,12 @@ class getid3_id3v2 extends getid3_handler return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-country'); } - - + /** + * @param string $languagecode + * @param bool $casesensitive + * + * @return string + */ public static function LanguageLookup($languagecode, $casesensitive=false) { if (!$casesensitive) { @@ -3081,7 +3143,11 @@ class getid3_id3v2 extends getid3_handler return getid3_lib::EmbeddedLookup($languagecode, $begin, __LINE__, __FILE__, 'id3v2-languagecode'); } - + /** + * @param int $index + * + * @return string + */ public static function ETCOEventLookup($index) { if (($index >= 0x17) && ($index <= 0xDF)) { return 'reserved for future use'; @@ -3125,6 +3191,11 @@ class getid3_id3v2 extends getid3_handler return (isset($EventLookup[$index]) ? $EventLookup[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public static function SYTLContentTypeLookup($index) { static $SYTLContentTypeLookup = array( 0x00 => 'other', @@ -3141,6 +3212,12 @@ class getid3_id3v2 extends getid3_handler return (isset($SYTLContentTypeLookup[$index]) ? $SYTLContentTypeLookup[$index] : ''); } + /** + * @param int $index + * @param bool $returnarray + * + * @return array|string + */ public static function APICPictureTypeLookup($index, $returnarray=false) { static $APICPictureTypeLookup = array( 0x00 => 'Other', @@ -3171,6 +3248,11 @@ class getid3_id3v2 extends getid3_handler return (isset($APICPictureTypeLookup[$index]) ? $APICPictureTypeLookup[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public static function COMRReceivedAsLookup($index) { static $COMRReceivedAsLookup = array( 0x00 => 'Other', @@ -3187,6 +3269,11 @@ class getid3_id3v2 extends getid3_handler return (isset($COMRReceivedAsLookup[$index]) ? $COMRReceivedAsLookup[$index] : ''); } + /** + * @param int $index + * + * @return string + */ public static function RVA2ChannelTypeLookup($index) { static $RVA2ChannelTypeLookup = array( 0x00 => 'Other', @@ -3203,6 +3290,11 @@ class getid3_id3v2 extends getid3_handler return (isset($RVA2ChannelTypeLookup[$index]) ? $RVA2ChannelTypeLookup[$index] : ''); } + /** + * @param string $framename + * + * @return string + */ public static function FrameNameLongLookup($framename) { $begin = __LINE__; @@ -3354,7 +3446,7 @@ class getid3_id3v2 extends getid3_handler TYER Year UFI Unique file identifier UFID Unique file identifier - ULT Unsychronised lyric/text transcription + ULT Unsynchronised lyric/text transcription USER Terms of use USLT Unsynchronised lyric/text transcription WAF Official audio file webpage @@ -3386,7 +3478,11 @@ class getid3_id3v2 extends getid3_handler // from http://privatewww.essex.ac.uk/~djmrob/replaygain/file_format_id3v2.html } - + /** + * @param string $framename + * + * @return string + */ public static function FrameNameShortLookup($framename) { $begin = __LINE__; @@ -3538,7 +3634,7 @@ class getid3_id3v2 extends getid3_handler TYER year UFI unique_file_identifier UFID unique_file_identifier - ULT unsychronised_lyric + ULT unsynchronised_lyric USER terms_of_use USLT unsynchronised_lyric WAF url_file @@ -3566,6 +3662,11 @@ class getid3_id3v2 extends getid3_handler return getid3_lib::EmbeddedLookup($framename, $begin, __LINE__, __FILE__, 'id3v2-framename_short'); } + /** + * @param string $encoding + * + * @return string + */ public static function TextEncodingTerminatorLookup($encoding) { // http://www.id3.org/id3v2.4.0-structure.txt // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings: @@ -3579,6 +3680,11 @@ class getid3_id3v2 extends getid3_handler return (isset($TextEncodingTerminatorLookup[$encoding]) ? $TextEncodingTerminatorLookup[$encoding] : "\x00"); } + /** + * @param int $encoding + * + * @return string + */ public static function TextEncodingNameLookup($encoding) { // http://www.id3.org/id3v2.4.0-structure.txt // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings: @@ -3592,26 +3698,66 @@ class getid3_id3v2 extends getid3_handler return (isset($TextEncodingNameLookup[$encoding]) ? $TextEncodingNameLookup[$encoding] : 'ISO-8859-1'); } + /** + * @param string $string + * @param string $terminator + * + * @return string + */ + public static function RemoveStringTerminator($string, $terminator) { + // Null terminator at end of comment string is somewhat ambiguous in the specification, may or may not be implemented by various taggers. Remove terminator only if present. + // https://github.com/JamesHeinrich/getID3/issues/121 + // https://community.mp3tag.de/t/x-trailing-nulls-in-id3v2-comments/19227 + if (substr($string, -strlen($terminator), strlen($terminator)) === $terminator) { + $string = substr($string, 0, -strlen($terminator)); + } + return $string; + } + + /** + * @param string $string + * + * @return string + */ + public static function MakeUTF16emptyStringEmpty($string) { + if (in_array($string, array("\x00", "\x00\x00", "\xFF\xFE", "\xFE\xFF"))) { + // if string only contains a BOM or terminator then make it actually an empty string + $string = ''; + } + return $string; + } + + /** + * @param string $framename + * @param int $id3v2majorversion + * + * @return bool|int + */ public static function IsValidID3v2FrameName($framename, $id3v2majorversion) { switch ($id3v2majorversion) { case 2: return preg_match('#[A-Z][A-Z0-9]{2}#', $framename); - break; case 3: case 4: return preg_match('#[A-Z][A-Z0-9]{3}#', $framename); - break; } return false; } + /** + * @param string $numberstring + * @param bool $allowdecimal + * @param bool $allownegative + * + * @return bool + */ public static function IsANumber($numberstring, $allowdecimal=false, $allownegative=false) { for ($i = 0; $i < strlen($numberstring); $i++) { - if ((chr($numberstring{$i}) < chr('0')) || (chr($numberstring{$i}) > chr('9'))) { - if (($numberstring{$i} == '.') && $allowdecimal) { + if ((chr($numberstring[$i]) < chr('0')) || (chr($numberstring[$i]) > chr('9'))) { + if (($numberstring[$i] == '.') && $allowdecimal) { // allowed - } elseif (($numberstring{$i} == '-') && $allownegative && ($i == 0)) { + } elseif (($numberstring[$i] == '-') && $allownegative && ($i == 0)) { // allowed } else { return false; @@ -3621,6 +3767,11 @@ class getid3_id3v2 extends getid3_handler return true; } + /** + * @param string $datestamp + * + * @return bool + */ public static function IsValidDateStampString($datestamp) { if (strlen($datestamp) != 8) { return false; @@ -3649,10 +3800,20 @@ class getid3_id3v2 extends getid3_handler return true; } + /** + * @param int $majorversion + * + * @return int + */ public static function ID3v2HeaderLength($majorversion) { return (($majorversion == 2) ? 6 : 10); } + /** + * @param string $frame_name + * + * @return string|false + */ public static function ID3v22iTunesBrokenFrameName($frame_name) { // iTunes (multiple versions) has been known to write ID3v2.3 style frames // but use ID3v2.2 frame names, right-padded using either [space] or [null] diff --git a/lib/getid3/module.tag.lyrics3.php b/lib/getid3/module.tag.lyrics3.php index 1645396..c8b2cf6 100644 --- a/lib/getid3/module.tag.lyrics3.php +++ b/lib/getid3/module.tag.lyrics3.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// /// // // module.tag.lyrics3.php // @@ -14,10 +14,14 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} class getid3_lyrics3 extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; @@ -29,8 +33,11 @@ class getid3_lyrics3 extends getid3_handler } $this->fseek((0 - 128 - 9 - 6), SEEK_END); // end - ID3v1 - "LYRICSEND" - [Lyrics3size] + $lyrics3offset = null; + $lyrics3version = null; + $lyrics3size = null; $lyrics3_id3v1 = $this->fread(128 + 9 + 6); - $lyrics3lsz = substr($lyrics3_id3v1, 0, 6); // Lyrics3size + $lyrics3lsz = (int) substr($lyrics3_id3v1, 0, 6); // Lyrics3size $lyrics3end = substr($lyrics3_id3v1, 6, 9); // LYRICSEND or LYRICS200 $id3v1tag = substr($lyrics3_id3v1, 15, 128); // ID3v1 @@ -61,7 +68,7 @@ class getid3_lyrics3 extends getid3_handler // Lyrics3v2, no ID3v1, no APE - $lyrics3size = strrev(substr(strrev($lyrics3_id3v1), 9, 6)) + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' + $lyrics3size = (int) strrev(substr(strrev($lyrics3_id3v1), 9, 6)) + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' $lyrics3offset = $info['filesize'] - $lyrics3size; $lyrics3version = 2; @@ -96,7 +103,7 @@ class getid3_lyrics3 extends getid3_handler } - if (isset($lyrics3offset)) { + if (isset($lyrics3offset) && isset($lyrics3version) && isset($lyrics3size)) { $info['avdataend'] = $lyrics3offset; $this->getLyrics3Data($lyrics3offset, $lyrics3version, $lyrics3size); @@ -105,7 +112,7 @@ class getid3_lyrics3 extends getid3_handler $GETID3_ERRORARRAY = &$info['warning']; getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_apetag = new getid3_apetag($getid3_temp); $getid3_apetag->overrideendoffset = $info['lyrics3']['tag_offset_start']; $getid3_apetag->Analyze(); @@ -126,6 +133,13 @@ class getid3_lyrics3 extends getid3_handler return true; } + /** + * @param int $endoffset + * @param int $version + * @param int $length + * + * @return bool + */ public function getLyrics3Data($endoffset, $version, $length) { // http://www.volweb.cz/str/tags.htm @@ -142,6 +156,8 @@ class getid3_lyrics3 extends getid3_handler } $rawdata = $this->fread($length); + $ParsedLyrics3 = array(); + $ParsedLyrics3['raw']['lyrics3version'] = $version; $ParsedLyrics3['raw']['lyrics3tagsize'] = $length; $ParsedLyrics3['tag_offset_start'] = $endoffset; @@ -229,7 +245,6 @@ class getid3_lyrics3 extends getid3_handler default: $this->error('Cannot process Lyrics3 version '.$version.' (only v1 and v2)'); return false; - break; } @@ -250,6 +265,11 @@ class getid3_lyrics3 extends getid3_handler return true; } + /** + * @param string $rawtimestamp + * + * @return int|false + */ public function Lyrics3Timestamp2Seconds($rawtimestamp) { if (preg_match('#^\\[([0-9]{2}):([0-9]{2})\\]$#', $rawtimestamp, $regs)) { return (int) (($regs[1] * 60) + $regs[2]); @@ -257,8 +277,14 @@ class getid3_lyrics3 extends getid3_handler return false; } + /** + * @param array $Lyrics3data + * + * @return bool + */ public function Lyrics3LyricsTimestampParse(&$Lyrics3data) { $lyricsarray = explode("\r\n", $Lyrics3data['raw']['LYR']); + $notimestamplyricsarray = array(); foreach ($lyricsarray as $key => $lyricline) { $regs = array(); unset($thislinetimestamps); @@ -287,6 +313,11 @@ class getid3_lyrics3 extends getid3_handler return true; } + /** + * @param string $char + * + * @return bool|null + */ public function IntString2Bool($char) { if ($char == '1') { return true; diff --git a/lib/getid3/module.tag.nikon-nctg.php b/lib/getid3/module.tag.nikon-nctg.php new file mode 100644 index 0000000..46e2f21 --- /dev/null +++ b/lib/getid3/module.tag.nikon-nctg.php @@ -0,0 +1,1448 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.tag.nikon-nctg.php // +// // +///////////////////////////////////////////////////////////////// + +/** + * Module for analyzing Nikon NCTG metadata in MOV files + * + * @author Pavel Starosek + * @author Phil Harvey + * + * @link https://exiftool.org/TagNames/Nikon.html#NCTG + * @link https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/Nikon.pm + * @link https://leo-van-stee.github.io/ + */ +class getid3_tag_nikon_nctg +{ + const EXIF_TYPE_UINT8 = 0x0001; + const EXIF_TYPE_CHAR = 0x0002; + const EXIF_TYPE_UINT16 = 0x0003; + const EXIF_TYPE_UINT32 = 0x0004; + const EXIF_TYPE_URATIONAL = 0x0005; + const EXIF_TYPE_INT8 = 0x0006; + const EXIF_TYPE_RAW = 0x0007; + const EXIF_TYPE_INT16 = 0x0008; + const EXIF_TYPE_INT32 = 0x0009; + const EXIF_TYPE_RATIONAL = 0x000A; + + protected static $exifTypeSizes = array( + self::EXIF_TYPE_UINT8 => 1, + self::EXIF_TYPE_CHAR => 1, + self::EXIF_TYPE_UINT16 => 2, + self::EXIF_TYPE_UINT32 => 4, + self::EXIF_TYPE_URATIONAL => 8, + self::EXIF_TYPE_INT8 => 1, + self::EXIF_TYPE_RAW => 1, + self::EXIF_TYPE_INT16 => 2, + self::EXIF_TYPE_INT32 => 4, + self::EXIF_TYPE_RATIONAL => 8, + ); + + protected static $exposurePrograms = array( + 0 => 'Not Defined', + 1 => 'Manual', + 2 => 'Program AE', + 3 => 'Aperture-priority AE', + 4 => 'Shutter speed priority AE', + 5 => 'Creative (Slow speed)', + 6 => 'Action (High speed)', + 7 => 'Portrait', + 8 => 'Landscape' + ); + + protected static $meteringModes = array( + 0 => 'Unknown', + 1 => 'Average', + 2 => 'Center-weighted average', + 3 => 'Spot', + 4 => 'Multi-spot', + 5 => 'Multi-segment', + 6 => 'Partial', + 255 => 'Other' + ); + + protected static $cropHiSpeeds = array( + 0 => 'Off', + 1 => '1.3x Crop', + 2 => 'DX Crop', + 3 => '5:4 Crop', + 4 => '3:2 Crop', + 6 => '16:9 Crop', + 8 => '2.7x Crop', + 9 => 'DX Movie Crop', + 10 => '1.3x Movie Crop', + 11 => 'FX Uncropped', + 12 => 'DX Uncropped', + 15 => '1.5x Movie Crop', + 17 => '1:1 Crop' + ); + + protected static $colorSpaces = array( + 1 => 'sRGB', + 2 => 'Adobe RGB' + ); + + protected static $vibrationReductions = array( + 1 => 'On', + 2 => 'Off' + ); + + protected static $VRModes = array( + 0 => 'Normal', + 1 => 'On (1)', + 2 => 'Active', + 3 => 'Sport' + ); + + protected static $activeDLightnings = array( + 0 => 'Off', + 1 => 'Low', + 3 => 'Normal', + 5 => 'High', + 7 => 'Extra High', + 8 => 'Extra High 1', + 9 => 'Extra High 2', + 10 => 'Extra High 3', + 11 => 'Extra High 4', + 65535 => 'Auto' + ); + + protected static $pictureControlDataAdjusts = array( + 0 => 'default', + 1 => 'quick', + 2 => 'full' + ); + + protected static $pictureControlDataFilterEffects = array( + 0x80 => 'off', + 0x81 => 'yellow', + 0x82 => 'orange', + 0x83 => 'red', + 0x84 => 'green', + 0xff => 'n/a' + ); + + protected static $pictureControlDataToningEffects = array( + 0x80 => 'b&w', + 0x81 => 'sepia', + 0x82 => 'cyanotype', + 0x83 => 'red', + 0x84 => 'yellow', + 0x85 => 'green', + 0x86 => 'blue-green', + 0x87 => 'blue', + 0x88 => 'purple-blue', + 0x89 => 'red-purple', + 0xff => 'n/a' + ); + + protected static $isoInfoExpansions = array( + 0x0000 => 'Off', + 0x0101 => 'Hi 0.3', + 0x0102 => 'Hi 0.5', + 0x0103 => 'Hi 0.7', + 0x0104 => 'Hi 1.0', + 0x0105 => 'Hi 1.3', + 0x0106 => 'Hi 1.5', + 0x0107 => 'Hi 1.7', + 0x0108 => 'Hi 2.0', + 0x0109 => 'Hi 2.3', + 0x010a => 'Hi 2.5', + 0x010b => 'Hi 2.7', + 0x010c => 'Hi 3.0', + 0x010d => 'Hi 3.3', + 0x010e => 'Hi 3.5', + 0x010f => 'Hi 3.7', + 0x0110 => 'Hi 4.0', + 0x0111 => 'Hi 4.3', + 0x0112 => 'Hi 4.5', + 0x0113 => 'Hi 4.7', + 0x0114 => 'Hi 5.0', + 0x0201 => 'Lo 0.3', + 0x0202 => 'Lo 0.5', + 0x0203 => 'Lo 0.7', + 0x0204 => 'Lo 1.0', + ); + + protected static $isoInfoExpansions2 = array( + 0x0000 => 'Off', + 0x0101 => 'Hi 0.3', + 0x0102 => 'Hi 0.5', + 0x0103 => 'Hi 0.7', + 0x0104 => 'Hi 1.0', + 0x0105 => 'Hi 1.3', + 0x0106 => 'Hi 1.5', + 0x0107 => 'Hi 1.7', + 0x0108 => 'Hi 2.0', + 0x0201 => 'Lo 0.3', + 0x0202 => 'Lo 0.5', + 0x0203 => 'Lo 0.7', + 0x0204 => 'Lo 1.0', + ); + + protected static $vignetteControls = array( + 0 => 'Off', + 1 => 'Low', + 3 => 'Normal', + 5 => 'High' + ); + + protected static $flashModes = array( + 0 => 'Did Not Fire', + 1 => 'Fired, Manual', + 3 => 'Not Ready', + 7 => 'Fired, External', + 8 => 'Fired, Commander Mode', + 9 => 'Fired, TTL Mode', + 18 => 'LED Light' + ); + + protected static $flashInfoSources = array( + 0 => 'None', + 1 => 'External', + 2 => 'Internal' + ); + + protected static $flashInfoExternalFlashFirmwares = array( + '0 0' => 'n/a', + '1 1' => '1.01 (SB-800 or Metz 58 AF-1)', + '1 3' => '1.03 (SB-800)', + '2 1' => '2.01 (SB-800)', + '2 4' => '2.04 (SB-600)', + '2 5' => '2.05 (SB-600)', + '3 1' => '3.01 (SU-800 Remote Commander)', + '4 1' => '4.01 (SB-400)', + '4 2' => '4.02 (SB-400)', + '4 4' => '4.04 (SB-400)', + '5 1' => '5.01 (SB-900)', + '5 2' => '5.02 (SB-900)', + '6 1' => '6.01 (SB-700)', + '7 1' => '7.01 (SB-910)', + ); + + protected static $flashInfoExternalFlashFlags = array( + 0 => 'Fired', + 2 => 'Bounce Flash', + 4 => 'Wide Flash Adapter', + 5 => 'Dome Diffuser', + ); + + protected static $flashInfoExternalFlashStatuses = array( + 0 => 'Flash Not Attached', + 1 => 'Flash Attached', + ); + + protected static $flashInfoExternalFlashReadyStates = array( + 0 => 'n/a', + 1 => 'Ready', + 6 => 'Not Ready', + ); + + protected static $flashInfoGNDistances = array( + 0 => 0, 19 => '2.8 m', + 1 => '0.1 m', 20 => '3.2 m', + 2 => '0.2 m', 21 => '3.6 m', + 3 => '0.3 m', 22 => '4.0 m', + 4 => '0.4 m', 23 => '4.5 m', + 5 => '0.5 m', 24 => '5.0 m', + 6 => '0.6 m', 25 => '5.6 m', + 7 => '0.7 m', 26 => '6.3 m', + 8 => '0.8 m', 27 => '7.1 m', + 9 => '0.9 m', 28 => '8.0 m', + 10 => '1.0 m', 29 => '9.0 m', + 11 => '1.1 m', 30 => '10.0 m', + 12 => '1.3 m', 31 => '11.0 m', + 13 => '1.4 m', 32 => '13.0 m', + 14 => '1.6 m', 33 => '14.0 m', + 15 => '1.8 m', 34 => '16.0 m', + 16 => '2.0 m', 35 => '18.0 m', + 17 => '2.2 m', 36 => '20.0 m', + 18 => '2.5 m', 255 => 'n/a' + ); + + protected static $flashInfoControlModes = array( + 0x00 => 'Off', + 0x01 => 'iTTL-BL', + 0x02 => 'iTTL', + 0x03 => 'Auto Aperture', + 0x04 => 'Automatic', + 0x05 => 'GN (distance priority)', + 0x06 => 'Manual', + 0x07 => 'Repeating Flash', + ); + + protected static $flashInfoColorFilters = array( + 0 => 'None', + 1 => 'FL-GL1 or SZ-2FL Fluorescent', + 2 => 'FL-GL2', + 9 => 'TN-A1 or SZ-2TN Incandescent', + 10 => 'TN-A2', + 65 => 'Red', + 66 => 'Blue', + 67 => 'Yellow', + 68 => 'Amber', + ); + + protected static $highISONoiseReductions = array( + 0 => 'Off', + 1 => 'Minimal', + 2 => 'Low', + 3 => 'Medium Low', + 4 => 'Normal', + 5 => 'Medium High', + 6 => 'High' + ); + + protected static $AFInfo2ContrastDetectAFChoices = array( + 0 => 'Off', + 1 => 'On', + 2 => 'On (2)' + ); + + protected static $AFInfo2AFAreaModesWithoutContrastDetectAF = array( + 0 => 'Single Area', + 1 => 'Dynamic Area', + 2 => 'Dynamic Area (closest subject)', + 3 => 'Group Dynamic', + 4 => 'Dynamic Area (9 points)', + 5 => 'Dynamic Area (21 points)', + 6 => 'Dynamic Area (51 points)', + 7 => 'Dynamic Area (51 points, 3D-tracking)', + 8 => 'Auto-area', + 9 => 'Dynamic Area (3D-tracking)', + 10 => 'Single Area (wide)', + 11 => 'Dynamic Area (wide)', + 12 => 'Dynamic Area (wide, 3D-tracking)', + 13 => 'Group Area', + 14 => 'Dynamic Area (25 points)', + 15 => 'Dynamic Area (72 points)', + 16 => 'Group Area (HL)', + 17 => 'Group Area (VL)', + 18 => 'Dynamic Area (49 points)', + 128 => 'Single', + 129 => 'Auto (41 points)', + 130 => 'Subject Tracking (41 points)', + 131 => 'Face Priority (41 points)', + 192 => 'Pinpoint', + 193 => 'Single', + 195 => 'Wide (S)', + 196 => 'Wide (L)', + 197 => 'Auto', + ); + + protected static $AFInfo2AFAreaModesWithContrastDetectAF = array( + 0 => 'Contrast-detect', + 1 => 'Contrast-detect (normal area)', + 2 => 'Contrast-detect (wide area)', + 3 => 'Contrast-detect (face priority)', + 4 => 'Contrast-detect (subject tracking)', + 128 => 'Single', + 129 => 'Auto (41 points)', + 130 => 'Subject Tracking (41 points)', + 131 => 'Face Priority (41 points)', + 192 => 'Pinpoint', + 193 => 'Single', + 194 => 'Dynamic', + 195 => 'Wide (S)', + 196 => 'Wide (L)', + 197 => 'Auto', + 198 => 'Auto (People)', + 199 => 'Auto (Animal)', + 200 => 'Normal-area AF', + 201 => 'Wide-area AF', + 202 => 'Face-priority AF', + 203 => 'Subject-tracking AF', + ); + + protected static $AFInfo2PhaseDetectAFChoices = array( + 0 => 'Off', + 1 => 'On (51-point)', + 2 => 'On (11-point)', + 3 => 'On (39-point)', + 4 => 'On (73-point)', + 5 => 'On (5)', + 6 => 'On (105-point)', + 7 => 'On (153-point)', + 8 => 'On (81-point)', + 9 => 'On (105-point)', + ); + + protected static $NikkorZLensIDS = array( + 1 => 'Nikkor Z 24-70mm f/4 S', + 2 => 'Nikkor Z 14-30mm f/4 S', + 4 => 'Nikkor Z 35mm f/1.8 S', + 8 => 'Nikkor Z 58mm f/0.95 S Noct', + 9 => 'Nikkor Z 50mm f/1.8 S', + 11 => 'Nikkor Z DX 16-50mm f/3.5-6.3 VR', + 12 => 'Nikkor Z DX 50-250mm f/4.5-6.3 VR', + 13 => 'Nikkor Z 24-70mm f/2.8 S', + 14 => 'Nikkor Z 85mm f/1.8 S', + 15 => 'Nikkor Z 24mm f/1.8 S', + 16 => 'Nikkor Z 70-200mm f/2.8 VR S', + 17 => 'Nikkor Z 20mm f/1.8 S', + 18 => 'Nikkor Z 24-200mm f/4-6.3 VR', + 21 => 'Nikkor Z 50mm f/1.2 S', + 22 => 'Nikkor Z 24-50mm f/4-6.3', + 23 => 'Nikkor Z 14-24mm f/2.8 S', + ); + + protected static $nikonTextEncodings = array( + 1 => 'UTF-8', + 2 => 'UTF-16' + ); + + /** + * Ref 4 + * + * @var int[][] + */ + protected static $decodeTables = array( + array( + 0xc1,0xbf,0x6d,0x0d,0x59,0xc5,0x13,0x9d,0x83,0x61,0x6b,0x4f,0xc7,0x7f,0x3d,0x3d, + 0x53,0x59,0xe3,0xc7,0xe9,0x2f,0x95,0xa7,0x95,0x1f,0xdf,0x7f,0x2b,0x29,0xc7,0x0d, + 0xdf,0x07,0xef,0x71,0x89,0x3d,0x13,0x3d,0x3b,0x13,0xfb,0x0d,0x89,0xc1,0x65,0x1f, + 0xb3,0x0d,0x6b,0x29,0xe3,0xfb,0xef,0xa3,0x6b,0x47,0x7f,0x95,0x35,0xa7,0x47,0x4f, + 0xc7,0xf1,0x59,0x95,0x35,0x11,0x29,0x61,0xf1,0x3d,0xb3,0x2b,0x0d,0x43,0x89,0xc1, + 0x9d,0x9d,0x89,0x65,0xf1,0xe9,0xdf,0xbf,0x3d,0x7f,0x53,0x97,0xe5,0xe9,0x95,0x17, + 0x1d,0x3d,0x8b,0xfb,0xc7,0xe3,0x67,0xa7,0x07,0xf1,0x71,0xa7,0x53,0xb5,0x29,0x89, + 0xe5,0x2b,0xa7,0x17,0x29,0xe9,0x4f,0xc5,0x65,0x6d,0x6b,0xef,0x0d,0x89,0x49,0x2f, + 0xb3,0x43,0x53,0x65,0x1d,0x49,0xa3,0x13,0x89,0x59,0xef,0x6b,0xef,0x65,0x1d,0x0b, + 0x59,0x13,0xe3,0x4f,0x9d,0xb3,0x29,0x43,0x2b,0x07,0x1d,0x95,0x59,0x59,0x47,0xfb, + 0xe5,0xe9,0x61,0x47,0x2f,0x35,0x7f,0x17,0x7f,0xef,0x7f,0x95,0x95,0x71,0xd3,0xa3, + 0x0b,0x71,0xa3,0xad,0x0b,0x3b,0xb5,0xfb,0xa3,0xbf,0x4f,0x83,0x1d,0xad,0xe9,0x2f, + 0x71,0x65,0xa3,0xe5,0x07,0x35,0x3d,0x0d,0xb5,0xe9,0xe5,0x47,0x3b,0x9d,0xef,0x35, + 0xa3,0xbf,0xb3,0xdf,0x53,0xd3,0x97,0x53,0x49,0x71,0x07,0x35,0x61,0x71,0x2f,0x43, + 0x2f,0x11,0xdf,0x17,0x97,0xfb,0x95,0x3b,0x7f,0x6b,0xd3,0x25,0xbf,0xad,0xc7,0xc5, + 0xc5,0xb5,0x8b,0xef,0x2f,0xd3,0x07,0x6b,0x25,0x49,0x95,0x25,0x49,0x6d,0x71,0xc7 + ), + array( + 0xa7,0xbc,0xc9,0xad,0x91,0xdf,0x85,0xe5,0xd4,0x78,0xd5,0x17,0x46,0x7c,0x29,0x4c, + 0x4d,0x03,0xe9,0x25,0x68,0x11,0x86,0xb3,0xbd,0xf7,0x6f,0x61,0x22,0xa2,0x26,0x34, + 0x2a,0xbe,0x1e,0x46,0x14,0x68,0x9d,0x44,0x18,0xc2,0x40,0xf4,0x7e,0x5f,0x1b,0xad, + 0x0b,0x94,0xb6,0x67,0xb4,0x0b,0xe1,0xea,0x95,0x9c,0x66,0xdc,0xe7,0x5d,0x6c,0x05, + 0xda,0xd5,0xdf,0x7a,0xef,0xf6,0xdb,0x1f,0x82,0x4c,0xc0,0x68,0x47,0xa1,0xbd,0xee, + 0x39,0x50,0x56,0x4a,0xdd,0xdf,0xa5,0xf8,0xc6,0xda,0xca,0x90,0xca,0x01,0x42,0x9d, + 0x8b,0x0c,0x73,0x43,0x75,0x05,0x94,0xde,0x24,0xb3,0x80,0x34,0xe5,0x2c,0xdc,0x9b, + 0x3f,0xca,0x33,0x45,0xd0,0xdb,0x5f,0xf5,0x52,0xc3,0x21,0xda,0xe2,0x22,0x72,0x6b, + 0x3e,0xd0,0x5b,0xa8,0x87,0x8c,0x06,0x5d,0x0f,0xdd,0x09,0x19,0x93,0xd0,0xb9,0xfc, + 0x8b,0x0f,0x84,0x60,0x33,0x1c,0x9b,0x45,0xf1,0xf0,0xa3,0x94,0x3a,0x12,0x77,0x33, + 0x4d,0x44,0x78,0x28,0x3c,0x9e,0xfd,0x65,0x57,0x16,0x94,0x6b,0xfb,0x59,0xd0,0xc8, + 0x22,0x36,0xdb,0xd2,0x63,0x98,0x43,0xa1,0x04,0x87,0x86,0xf7,0xa6,0x26,0xbb,0xd6, + 0x59,0x4d,0xbf,0x6a,0x2e,0xaa,0x2b,0xef,0xe6,0x78,0xb6,0x4e,0xe0,0x2f,0xdc,0x7c, + 0xbe,0x57,0x19,0x32,0x7e,0x2a,0xd0,0xb8,0xba,0x29,0x00,0x3c,0x52,0x7d,0xa8,0x49, + 0x3b,0x2d,0xeb,0x25,0x49,0xfa,0xa3,0xaa,0x39,0xa7,0xc5,0xa7,0x50,0x11,0x36,0xfb, + 0xc6,0x67,0x4a,0xf5,0xa5,0x12,0x65,0x7e,0xb0,0xdf,0xaf,0x4e,0xb3,0x61,0x7f,0x2f + ) + ); + + /** + * @var getID3 + */ + private $getid3; + + public function __construct(getID3 $getid3) + { + $this->getid3 = $getid3; + } + + /** + * Get a copy of all NCTG tags extracted from the video + * + * @param string $atomData + * + * @return array + */ + public function parse($atomData) { + // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 + // Data is stored as records of: + // * 4 bytes record type + // * 2 bytes size of data field type: + // 0x0001 = flag / unsigned byte (size field *= 1-byte) + // 0x0002 = char / ascii strings (size field *= 1-byte) + // 0x0003 = DWORD+ / unsigned short (size field *= 2-byte), values are stored CDAB + // 0x0004 = QWORD+ / unsigned long (size field *= 4-byte), values are stored EFGHABCD + // 0x0005 = float / unsigned rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // 0x0006 = signed byte (size field *= 1-byte) + // 0x0007 = raw bytes (size field *= 1-byte) + // 0x0008 = signed short (size field *= 2-byte), values are stored as CDAB + // 0x0009 = signed long (size field *= 4-byte), values are stored as EFGHABCD + // 0x000A = float / signed rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // * 2 bytes data size field + // * ? bytes data (string data may be null-padded; datestamp fields are in the format "2011:05:25 20:24:15") + // all integers are stored BigEndian + + $NCTGtagName = array( + 0x00000001 => 'Make', + 0x00000002 => 'Model', + 0x00000003 => 'Software', + 0x00000011 => 'CreateDate', + 0x00000012 => 'DateTimeOriginal', + 0x00000013 => 'FrameCount', + 0x00000016 => 'FrameRate', + 0x00000019 => 'TimeZone', + 0x00000022 => 'FrameWidth', + 0x00000023 => 'FrameHeight', + 0x00000032 => 'AudioChannels', + 0x00000033 => 'AudioBitsPerSample', + 0x00000034 => 'AudioSampleRate', + 0x00001002 => 'NikonDateTime', + 0x00001013 => 'ElectronicVR', + 0x0110829a => 'ExposureTime', + 0x0110829d => 'FNumber', + 0x01108822 => 'ExposureProgram', + 0x01109204 => 'ExposureCompensation', + 0x01109207 => 'MeteringMode', + 0x0110920a => 'FocalLength', // mm + 0x0110a431 => 'SerialNumber', + 0x0110a432 => 'LensInfo', + 0x0110a433 => 'LensMake', + 0x0110a434 => 'LensModel', + 0x0110a435 => 'LensSerialNumber', + 0x01200000 => 'GPSVersionID', + 0x01200001 => 'GPSLatitudeRef', + 0x01200002 => 'GPSLatitude', + 0x01200003 => 'GPSLongitudeRef', + 0x01200004 => 'GPSLongitude', + 0x01200005 => 'GPSAltitudeRef', // 0 = Above Sea Level, 1 = Below Sea Level + 0x01200006 => 'GPSAltitude', + 0x01200007 => 'GPSTimeStamp', + 0x01200008 => 'GPSSatellites', + 0x01200010 => 'GPSImgDirectionRef', // M = Magnetic North, T = True North + 0x01200011 => 'GPSImgDirection', + 0x01200012 => 'GPSMapDatum', + 0x0120001d => 'GPSDateStamp', + 0x02000001 => 'MakerNoteVersion', + 0x02000005 => 'WhiteBalance', + 0x02000007 => 'FocusMode', + 0x0200000b => 'WhiteBalanceFineTune', + 0x0200001b => 'CropHiSpeed', + 0x0200001e => 'ColorSpace', + 0x0200001f => 'VRInfo', + 0x02000022 => 'ActiveDLighting', + 0x02000023 => 'PictureControlData', + 0x02000024 => 'WorldTime', + 0x02000025 => 'ISOInfo', + 0x0200002a => 'VignetteControl', + 0x0200002c => 'UnknownInfo', + 0x02000032 => 'UnknownInfo2', + 0x02000039 => 'LocationInfo', + 0x02000083 => 'LensType', + 0x02000084 => 'Lens', + 0x02000087 => 'FlashMode', + 0x02000098 => 'LensData', + 0x020000a7 => 'ShutterCount', + 0x020000a8 => 'FlashInfo', + 0x020000ab => 'VariProgram', + 0x020000b1 => 'HighISONoiseReduction', + 0x020000b7 => 'AFInfo2', + 0x020000c3 => 'BarometerInfo', + ); + + $firstPassNeededTags = array( + 0x00000002, // Model + 0x0110a431, // SerialNumber + 0x020000a7, // ShutterCount + ); + + $datalength = strlen($atomData); + $parsed = array(); + $model = $serialNumber = $shutterCount = null; + for ($pass = 0; $pass < 2; ++$pass) { + $offset = 0; + $parsed = array(); + $data = null; + while ($offset < $datalength) { + $record_type = getid3_lib::BigEndian2Int(substr($atomData, $offset, 4)); + $offset += 4; + $data_size_type = getid3_lib::BigEndian2Int(substr($atomData, $offset, 2)); + $data_size = static::$exifTypeSizes[$data_size_type]; + $offset += 2; + $data_count = getid3_lib::BigEndian2Int(substr($atomData, $offset, 2)); + $offset += 2; + $data = array(); + + if ($pass === 0 && !in_array($record_type, $firstPassNeededTags, true)) { + $offset += $data_count * $data_size; + continue; + } + + switch ($data_size_type) { + case self::EXIF_TYPE_UINT8: // 0x0001 = flag / unsigned byte (size field *= 1-byte) + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_CHAR: // 0x0002 = char / ascii strings (size field *= 1-byte) + $data = substr($atomData, $offset, $data_count * $data_size); + $offset += ($data_count * $data_size); + $data = rtrim($data, "\x00"); + break; + case self::EXIF_TYPE_UINT16: // 0x0003 = DWORD+ / unsigned short (size field *= 2-byte), values are stored CDAB + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_UINT32: // 0x0004 = QWORD+ / unsigned long (size field *= 4-byte), values are stored EFGHABCD + // нужно проверить FrameCount + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_URATIONAL: // 0x0005 = float / unsigned rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + for ($i = 0; $i < $data_count; ++$i) { + $numerator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 0, 4)); + $denomninator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 4, 4)); + if ($denomninator == 0) { + $data[] = false; + } else { + $data[] = (float)$numerator / $denomninator; + } + } + $offset += ($data_size * $data_count); + break; + case self::EXIF_TYPE_INT8: // 0x0006 = bytes / signed byte (size field *= 1-byte) + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $data[] = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size), false, true); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_RAW: // 0x0007 = raw bytes (size field *= 1-byte) + $data = substr($atomData, $offset, $data_count * $data_size); + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_INT16: // 0x0008 = signed short (size field *= 2-byte), values are stored as CDAB + for ($i = 0; $i < $data_count; ++$i) { + $value = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size)); + if ($value >= 0x8000) { + $value -= 0x10000; + } + $data[] = $value; + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_INT32: // 0x0009 = signed long (size field *= 4-byte), values are stored as EFGHABCD + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $data = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size), $data_size), false, true); + } + $offset += ($data_count * $data_size); + break; + case self::EXIF_TYPE_RATIONAL: // 0x000A = float / signed rational (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together + // NOT TESTED + for ($i = 0; $i < $data_count; ++$i) { + $numerator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 0, 4), false, true); + $denomninator = getid3_lib::BigEndian2Int(substr($atomData, $offset + ($i * $data_size) + 4, 4), false, true); + if ($denomninator == 0) { + $data[] = false; + } else { + $data[] = (float)$numerator / $denomninator; + } + } + $offset += ($data_size * $data_count); + if (count($data) == 1) { + $data = $data[0]; + } + break; + default: + $this->getid3->warning('QuicktimeParseNikonNCTG()::unknown $data_size_type: ' . $data_size_type); + break 2; + } + + if (is_array($data) && count($data) === 1) { + $data = $data[0]; + } + + switch ($record_type) { + case 0x00000002: + $model = $data; + break; + case 0x00000013: // FrameCount + if (is_array($data) && count($data) === 2 && $data[1] == 0) { + $data = $data[0]; + } + break; + case 0x00000011: // CreateDate + case 0x00000012: // DateTimeOriginal + case 0x00001002: // NikonDateTime + $data = strtotime($data); + break; + case 0x00001013: // ElectronicVR + $data = (bool) $data; + break; + case 0x0110829a: // ExposureTime + // Print exposure time as a fraction + /** @var float $data */ + if ($data < 0.25001 && $data > 0) { + $data = sprintf("1/%d", intval(0.5 + 1 / $data)); + } + break; + case 0x01109204: // ExposureCompensation + $data = $this->printFraction($data); + break; + case 0x01108822: // ExposureProgram + $data = isset(static::$exposurePrograms[$data]) ? static::$exposurePrograms[$data] : $data; + break; + case 0x01109207: // MeteringMode + $data = isset(static::$meteringModes[$data]) ? static::$meteringModes[$data] : $data; + break; + case 0x0110a431: // SerialNumber + $serialNumber = $this->serialKey($data, $model); + break; + case 0x01200000: // GPSVersionID + $parsed['GPS']['computed']['version'] = 'v'.implode('.', $data); + break; + case 0x01200002: // GPSLatitude + if (is_array($data)) { + $direction_multiplier = ((isset($parsed['GPSLatitudeRef']) && ($parsed['GPSLatitudeRef'] === 'S')) ? -1 : 1); + $parsed['GPS']['computed']['latitude'] = $direction_multiplier * ($data[0] + ($data[1] / 60) + ($data[2] / 3600)); + } + break; + case 0x01200004: // GPSLongitude + if (is_array($data)) { + $direction_multiplier = ((isset($parsed['GPSLongitudeRef']) && ($parsed['GPSLongitudeRef'] === 'W')) ? -1 : 1); + $parsed['GPS']['computed']['longitude'] = $direction_multiplier * ($data[0] + ($data[1] / 60) + ($data[2] / 3600)); + } + break; + case 0x01200006: // GPSAltitude + if (isset($parsed['GPSAltitudeRef'])) { + $direction_multiplier = (!empty($parsed['GPSAltitudeRef']) ? -1 : 1); // 0 = above sea level; 1 = below sea level + $parsed['GPS']['computed']['altitude'] = $direction_multiplier * $data; + } + break; + case 0x0120001d: // GPSDateStamp + if (isset($parsed['GPSTimeStamp']) && is_array($parsed['GPSTimeStamp']) && $data !== '') { + $explodedDate = explode(':', $data); + $parsed['GPS']['computed']['timestamp'] = gmmktime($parsed['GPSTimeStamp'][0], $parsed['GPSTimeStamp'][1], $parsed['GPSTimeStamp'][2], $explodedDate[1], $explodedDate[2], $explodedDate[0]); + } + break; + case 0x02000001: // MakerNoteVersion + $data = ltrim(substr($data, 0, 2) . '.' . substr($data, 2, 2), '0'); + break; + case 0x0200001b: // CropHiSpeed + if (is_array($data) && count($data) === 7) { + $name = isset(static::$cropHiSpeeds[$data[0]]) ? static::$cropHiSpeeds[$data[0]] : sprintf('Unknown (%d)', $data[0]); + $data = array( + 'Name' => $name, + 'OriginalWidth' => $data[1], + 'OriginalHeight' => $data[2], + 'CroppedWidth' => $data[3], + 'CroppedHeight' => $data[4], + 'PixelXPosition' => $data[5], + 'PixelYPosition' => $data[6], + ); + } + break; + case 0x0200001e: // ColorSpace + $data = isset(static::$colorSpaces[$data]) ? static::$colorSpaces[$data] : $data; + break; + case 0x0200001f: // VRInfo + $data = array( + 'VRInfoVersion' => substr($data, 0, 4), + 'VibrationReduction' => isset(static::$vibrationReductions[ord(substr($data, 4, 1))]) + ? static::$vibrationReductions[ord(substr($data, 4, 1))] + : null, + 'VRMode' => static::$VRModes[ord(substr($data, 6, 1))], + ); + break; + case 0x02000022: // ActiveDLighting + $data = isset(static::$activeDLightnings[$data]) ? static::$activeDLightnings[$data] : $data; + break; + case 0x02000023: // PictureControlData + switch (substr($data, 0, 2)) { + case '01': + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + 'PictureControlName' => rtrim(substr($data, 4, 20), "\x00"), + 'PictureControlBase' => rtrim(substr($data, 24, 20), "\x00"), + //'?' => substr($data, 44, 4), + 'PictureControlAdjust' => static::$pictureControlDataAdjusts[ord(substr($data, 48, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 49, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 50, 1)) - 0x80, 'No Sharpening', '%d'), + 'Contrast' => $this->printPC(ord(substr($data, 51, 1)) - 0x80), + 'Brightness' => $this->printPC(ord(substr($data, 52, 1)) - 0x80), + 'Saturation' => $this->printPC(ord(substr($data, 53, 1)) - 0x80), + 'HueAdjustment' => $this->printPC(ord(substr($data, 54, 1)) - 0x80, 'None'), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 55, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 56, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 57, 1)) - 0x80), + ); + break; + case '02': + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + 'PictureControlName' => rtrim(substr($data, 4, 20), "\x00"), + 'PictureControlBase' => rtrim(substr($data, 24, 20), "\x00"), + //'?' => substr($data, 44, 4), + 'PictureControlAdjust' => static::$pictureControlDataAdjusts[ord(substr($data, 48, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 49, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 51, 1)) - 0x80, 'None', '%.2f', 4), + 'Clarity' => $this->printPC(ord(substr($data, 53, 1)) - 0x80, 'None', '%.2f', 4), + 'Contrast' => $this->printPC(ord(substr($data, 55, 1)) - 0x80, 'None', '%.2f', 4), + 'Brightness' => $this->printPC(ord(substr($data, 57, 1)) - 0x80, 'Normal', '%.2f', 4), + 'Saturation' => $this->printPC(ord(substr($data, 59, 1)) - 0x80, 'None', '%.2f', 4), + 'Hue' => $this->printPC(ord(substr($data, 61, 1)) - 0x80, 'None', '%.2f', 4), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 63, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 64, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 65, 1)) - 0x80, 'None', '%.2f', 4), + ); + break; + case '03': + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + 'PictureControlName' => rtrim(substr($data, 8, 20), "\x00"), + 'PictureControlBase' => rtrim(substr($data, 28, 20), "\x00"), + 'PictureControlAdjust' => static::$pictureControlDataAdjusts[ord(substr($data, 54, 1))], + 'PictureControlQuickAdjust' => $this->printPC(ord(substr($data, 55, 1)) - 0x80), + 'Sharpness' => $this->printPC(ord(substr($data, 57, 1)) - 0x80, 'None', '%.2f', 4), + 'MidRangeSharpness' => $this->printPC(ord(substr($data, 59, 1)) - 0x80, 'None', '%.2f', 4), + 'Clarity' => $this->printPC(ord(substr($data, 61, 1)) - 0x80, 'None', '%.2f', 4), + 'Contrast' => $this->printPC(ord(substr($data, 63, 1)) - 0x80, 'None', '%.2f', 4), + 'Brightness' => $this->printPC(ord(substr($data, 65, 1)) - 0x80, 'Normal', '%.2f', 4), + 'Saturation' => $this->printPC(ord(substr($data, 67, 1)) - 0x80, 'None', '%.2f', 4), + 'Hue' => $this->printPC(ord(substr($data, 69, 1)) - 0x80, 'None', '%.2f', 4), + 'FilterEffect' => static::$pictureControlDataFilterEffects[ord(substr($data, 71, 1))], + 'ToningEffect' => static::$pictureControlDataToningEffects[ord(substr($data, 72, 1))], + 'ToningSaturation' => $this->printPC(ord(substr($data, 73, 1)) - 0x80, 'None', '%.2f', 4), + ); + break; + default: + $data = array( + 'PictureControlVersion' => substr($data, 0, 4), + ); + break; + } + break; + case 0x02000024: // WorldTime + // https://exiftool.org/TagNames/Nikon.html#WorldTime + // timezone is stored as offset from GMT in minutes + $timezone = getid3_lib::BigEndian2Int(substr($data, 0, 2)); + if ($timezone & 0x8000) { + $timezone = 0 - (0x10000 - $timezone); + } + $hours = (int)abs($timezone / 60); + $minutes = abs($timezone) - $hours * 60; + + $dst = (bool)getid3_lib::BigEndian2Int(substr($data, 2, 1)); + switch (getid3_lib::BigEndian2Int(substr($data, 3, 1))) { + case 2: + $datedisplayformat = 'D/M/Y'; + break; + case 1: + $datedisplayformat = 'M/D/Y'; + break; + case 0: + default: + $datedisplayformat = 'Y/M/D'; + break; + } + + $data = array( + 'timezone' => sprintf('%s%02d:%02d', $timezone >= 0 ? '+' : '-', $hours, $minutes), + 'dst' => $dst, + 'display' => $datedisplayformat + ); + break; + case 0x02000025: // ISOInfo + $data = array( + 'ISO' => (int)ceil(100 * pow(2, ord(substr($data, 0, 1)) / 12 - 5)), + 'ISOExpansion' => static::$isoInfoExpansions[getid3_lib::BigEndian2Int(substr($data, 4, 2))], + 'ISO2' => (int)ceil(100 * pow(2, ord(substr($data, 6, 1)) / 12 - 5)), + 'ISOExpansion2' => static::$isoInfoExpansions2[getid3_lib::BigEndian2Int(substr($data, 10, 2))] + ); + break; + case 0x0200002a: // VignetteControl + $data = isset(static::$vignetteControls[$data]) ? static::$vignetteControls[$data] : $data; + break; + case 0x0200002c: // UnknownInfo + $data = array( + 'UnknownInfoVersion' => substr($data, 0, 4), + ); + break; + case 0x02000032: // UnknownInfo2 + $data = array( + 'UnknownInfo2Version' => substr($data, 0, 4), + ); + break; + case 0x02000039: // LocationInfo + $encoding = isset(static::$nikonTextEncodings[ord(substr($data, 4, 1))]) + ? static::$nikonTextEncodings[ord(substr($data, 4, 1))] + : null; + $data = array( + 'LocationInfoVersion' => substr($data, 0, 4), + 'TextEncoding' => $encoding, + 'CountryCode' => trim(substr($data, 5, 3), "\x00"), + 'POILevel' => ord(substr($data, 8, 1)), + 'Location' => getid3_lib::iconv_fallback($encoding, $this->getid3->info['encoding'], substr($data, 9, 70)), + ); + break; + case 0x02000083: // LensType + if ($data) { + $decodedBits = array( + '1' => (bool) (($data >> 4) & 1), + 'MF' => (bool) (($data >> 0) & 1), + 'D' => (bool) (($data >> 1) & 1), + 'E' => (bool) (($data >> 6) & 1), + 'G' => (bool) (($data >> 2) & 1), + 'VR' => (bool) (($data >> 3) & 1), + '[7]' => (bool) (($data >> 7) & 1), // AF-P? + '[8]' => (bool) (($data >> 5) & 1) // FT-1? + ); + if ($decodedBits['D'] === true && $decodedBits['G'] === true) { + $decodedBits['D'] = false; + } + } else { + $decodedBits = array('AF' => true); + } + $data = $decodedBits; + break; + case 0x0110a432: // LensInfo + case 0x02000084: // Lens + if (count($data) !== 4) { + break; + } + + $value = $data[0]; + if ($data[1] && $data[1] !== $data[0]) { + $value .= '-' . $data[1]; + } + $value .= 'mm f/' . $data[2]; + if ($data[3] && $data[3] !== $data[2]) { + $value .= '-' . $data[3]; + } + $data = $value; + break; + case 0x02000087: // FlashMode + $data = isset(static::$flashModes[$data]) ? static::$flashModes[$data] : $data; + break; + case 0x02000098: // LensData + $version = substr($data, 0, 4); + + switch ($version) { + case '0100': + $data = array( + 'LensDataVersion' => $version, + 'LensIDNumber' => ord(substr($data, 6, 1)), + 'LensFStops' => ord(substr($data, 7, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 8, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 9, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 10, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 11, 1)) / 24), + 'MCUVersion' => ord(substr($data, 12, 1)), + ); + break; + case '0101': + case '0201': + case '0202': + case '0203': + $isEncrypted = $version !== '0101'; + if ($isEncrypted) { + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + } + + $data = array( + 'LensDataVersion' => $version, + 'ExitPupilPosition' => ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0, // mm + 'AFAperture' => pow(2, ord(substr($data, 5, 1)) / 24), + 'FocusPosition' => '0x' . str_pad(strtoupper(dechex(ord(substr($data, 8, 1)))), 2, '0', STR_PAD_LEFT), + 'FocusDistance' => 0.01 * pow(10, ord(substr($data, 9, 1)) / 40), // m + 'FocalLength' => 5 * pow(2, ord(substr($data, 10, 1)) / 24), // mm + 'LensIDNumber' => ord(substr($data, 11, 1)), + 'LensFStops' => ord(substr($data, 12, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 13, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 14, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 15, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 16, 1)) / 24), + 'MCUVersion' => ord(substr($data, 17, 1)), + 'EffectiveMaxAperture' => pow(2, ord(substr($data, 18, 1)) / 24), + ); + break; + case '0204': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'ExitPupilPosition' => ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0, // mm + 'AFAperture' => pow(2, ord(substr($data, 5, 1)) / 24), + 'FocusPosition' => '0x' . str_pad(strtoupper(dechex(ord(substr($data, 8, 1)))), 2, '0', STR_PAD_LEFT), + 'FocusDistance' => 0.01 * pow(10, ord(substr($data, 10, 1)) / 40), // m + 'FocalLength' => 5 * pow(2, ord(substr($data, 11, 1)) / 24), // mm + 'LensIDNumber' => ord(substr($data, 12, 1)), + 'LensFStops' => ord(substr($data, 13, 1)) / 12, + 'MinFocalLength' => 5 * pow(2, ord(substr($data, 14, 1)) / 24), // mm + 'MaxFocalLength' => 5 * pow(2, ord(substr($data, 15, 1)) / 24), // mm + 'MaxApertureAtMinFocal' => pow(2, ord(substr($data, 16, 1)) / 24), + 'MaxApertureAtMaxFocal' => pow(2, ord(substr($data, 17, 1)) / 24), + 'MCUVersion' => ord(substr($data, 18, 1)), + 'EffectiveMaxAperture' => pow(2, ord(substr($data, 19, 1)) / 24), + ); + break; + case '0400': + case '0401': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 394, 64), + ); + break; + case '0402': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 395, 64), + ); + break; + case '0403': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + 'LensModel' => substr($data, 684, 64), + ); + break; + case '0800': + case '0801': + $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $newData = array( + 'LensDataVersion' => $version, + ); + + if (!preg_match('#^.\0+#s', substr($data, 3, 17))) { + $newData['ExitPupilPosition'] = ord(substr($data, 4, 1)) > 0 ? 2048 / ord(substr($data, 4, 1)) : 0; // mm + $newData['AFAperture'] = pow(2, ord(substr($data, 5, 1)) / 24); + $newData['FocusPosition'] = '0x' . str_pad(strtoupper(dechex(ord(substr($data, 9, 1)))), 2, '0', STR_PAD_LEFT); + $newData['FocusDistance'] = 0.01 * pow(10, ord(substr($data, 11, 1)) / 40); // m + $newData['FocalLength'] = 5 * pow(2, ord(substr($data, 12, 1)) / 24); // mm + $newData['LensIDNumber'] = ord(substr($data, 13, 1)); + $newData['LensFStops'] = ord(substr($data, 14, 1)) / 12; + $newData['MinFocalLength'] = 5 * pow(2, ord(substr($data, 15, 1)) / 24); // mm + $newData['MaxFocalLength'] = 5 * pow(2, ord(substr($data, 16, 1)) / 24); // mm + $newData['MaxApertureAtMinFocal'] = pow(2, ord(substr($data, 17, 1)) / 24); + $newData['MaxApertureAtMaxFocal'] = pow(2, ord(substr($data, 18, 1)) / 24); + $newData['MCUVersion'] = ord(substr($data, 19, 1)); + $newData['EffectiveMaxAperture'] = pow(2, ord(substr($data, 20, 1)) / 24); + } + + if (!preg_match('#^.\0+#s', substr($data, 47, 17))) { + $newData['LensID'] = static::$NikkorZLensIDS[getid3_lib::LittleEndian2Int(substr($data, 48, 2))]; + $newData['MaxAperture'] = pow(2, (getid3_lib::LittleEndian2Int(substr($data, 54, 2)) / 384 - 1)); + $newData['FNumber'] = pow(2, (getid3_lib::LittleEndian2Int(substr($data, 56, 2)) / 384 - 1)); + $newData['FocalLength'] = getid3_lib::LittleEndian2Int(substr($data, 60, 2)); // mm + $newData['FocusDistance'] = 0.01 * pow(10, ord(substr($data, 79, 1)) / 40); // m + } + + $data = $newData; + break; + default: + // $data = $this->decryptLensInfo($data, $serialNumber, $shutterCount, 4); + + $data = array( + 'LensDataVersion' => $version, + ); + break; + } + break; + case 0x020000a7: // ShutterCount + $shutterCount = $data; + break; + case 0x020000a8: // FlashInfo + $version = substr($data, 0, 4); + + switch ($version) { + case '0100': + case '0101': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 11, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 12, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 13, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 14, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 15, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 16, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 15, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 17, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 17, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 16, 1)) & 0xF0) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 18, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 18, 1), false, true) / 6), + ); + break; + case '0102': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 16, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 16, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 18, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 18, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 17, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 19, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 19, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 20, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 20, 1), false, true) / 6), + ); + break; + case '0103': + case '0104': + case '0105': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 10, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashColorFilter' => static::$flashInfoColorFilters[ord(substr($data, 16, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 19, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 19, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 20, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 20, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 21, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 21, 1), false, true) / 6), + 'ExternalFlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 27, 1), false, true) / 6), + 'FlashExposureComp3' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 29, 1), false, true) / 6), + 'FlashExposureComp4' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 39, 1), false, true) / 6), + ); + break; + case '0106': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashFlags' => $this->externalFlashFlagsLookup(ord(substr($data, 8, 1))), + 'FlashCommanderMode' => (bool)(ord(substr($data, 9, 1)) & 0x80), + 'FlashControlMode' => static::$flashInfoControlModes[ord(substr($data, 9, 1)) & 0x7F], + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => self::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashColorFilter' => static::$flashInfoColorFilters[ord(substr($data, 16, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashOutput' => (ord(substr($data, 9, 1)) & 0x7F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 39, 1)) / 6) * 100)) : 0, + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 39, 1), false, true) / 6), + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 40, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 40, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 41, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 41, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 42, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 42, 1), false, true) / 6), + ); + break; + case '0107': + case '0108': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'ExternalFlashZoomOverride' => (bool)(ord(substr($data, 8, 1)) & 0x80), + 'ExternalFlashStatus' => static::$flashInfoExternalFlashStatuses[ord(substr($data, 8, 1)) & 0x01], + 'ExternalFlashReadyState' => static::$flashInfoExternalFlashReadyStates[ord(substr($data, 9, 1)) & 0x07], + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 10, 1), false, true) / 6), + 'FlashFocalLength' => ord(substr($data, 12, 1)), // mm + 'RepeatingFlashRate' => ord(substr($data, 13, 1)), // Hz + 'RepeatingFlashCount' => ord(substr($data, 14, 1)), + 'FlashGNDistance' => static::$flashInfoGNDistances[ord(substr($data, 15, 1))], + 'FlashGroupAControlMode' => static::$flashInfoControlModes[ord(substr($data, 17, 1)) & 0x0F], + 'FlashGroupBControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0xF0], + 'FlashGroupCControlMode' => static::$flashInfoControlModes[ord(substr($data, 18, 1)) & 0x0F], + 'FlashGroupAOutput' => (ord(substr($data, 17, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 40, 1)) / 6) * 100)) : 0, + 'FlashGroupACompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 40, 1), false, true) / 6), + 'FlashGroupBOutput' => (ord(substr($data, 18, 1)) & 0xF0) >= 0x60 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 41, 1)) / 6) * 100)) : 0, + 'FlashGroupBCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 41, 1), false, true) / 6), + 'FlashGroupCOutput' => (ord(substr($data, 18, 1)) & 0x0F) >= 0x06 ? sprintf('%.0f%%', pow(2, (-ord(substr($data, 42, 1)) / 6) * 100)) : 0, + 'FlashGroupCCompensation' => sprintf('%+.1f', -getid3_lib::BigEndian2Int(substr($data, 42, 1), false, true) / 6), + ); + break; + case '0300': + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + 'FlashSource' => static::$flashInfoSources[ord(substr($data, 4, 1))], + 'ExternalFlashFirmware' => $this->flashFirmwareLookup(ord(substr($data, 6, 1)), ord(substr($data, 7, 1))), + 'FlashCompensation' => $this->printFraction(-getid3_lib::BigEndian2Int(substr($data, 27, 1), false, true) / 6), + ); + break; + default: + $data = array( + 'FlashInfoVersion' => substr($data, 0, 4), + ); + break; + } + break; + case 0x020000b1: // HighISONoiseReduction + $data = isset(static::$highISONoiseReductions[$data]) ? static::$highISONoiseReductions[$data] : $data; + break; + case 0x020000b7: // AFInfo2 + $avInfo2Version = substr($data, 0, 4); + $contrastDetectAF = ord(substr($data, 4, 1)); + $phaseDetectAF = ord(substr($data, 6, 1)); + $rows = array( + 'AFInfo2Version' => $avInfo2Version, + 'ContrastDetectAF' => static::$AFInfo2ContrastDetectAFChoices[$contrastDetectAF], + 'AFAreaMode' => $contrastDetectAF + ? static::$AFInfo2AFAreaModesWithContrastDetectAF[ord(substr($data, 5, 1))] + : static::$AFInfo2AFAreaModesWithoutContrastDetectAF[ord(substr($data, 5, 1))], + 'PhaseDetectAF' => static::$AFInfo2PhaseDetectAFChoices[$phaseDetectAF], + ); + + if ($avInfo2Version === '0100') { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 16, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 18, 2)); + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 20, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 22, 2)); + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 24, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 26, 2)); + $rows['ContrastDetectAFInFocus'] = (bool)ord(substr($data, 28, 1)); + } elseif (strpos($avInfo2Version, '03') === 0) { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 42, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 44, 2)); + if ($contrastDetectAF === 2 + || ($contrastDetectAF === 1 && $avInfo2Version === '0301') + ) { + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 46, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 48, 2)); + } + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 50, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 52, 2)); + } elseif ($contrastDetectAF === 1 && $avInfo2Version === '0101') { + $rows['AFImageWidth'] = getid3_lib::BigEndian2Int(substr($data, 70, 2)); + $rows['AFImageHeight'] = getid3_lib::BigEndian2Int(substr($data, 72, 2)); + $rows['AFAreaXPosition'] = getid3_lib::BigEndian2Int(substr($data, 74, 2)); + $rows['AFAreaYPosition'] = getid3_lib::BigEndian2Int(substr($data, 76, 2)); + $rows['AFAreaWidth'] = getid3_lib::BigEndian2Int(substr($data, 78, 2)); + $rows['AFAreaHeight'] = getid3_lib::BigEndian2Int(substr($data, 80, 2)); + $rows['ContrastDetectAFInFocus'] = (bool) ord(substr($data, 82, 1)); + } + + $data = $rows; + break; + case 0x020000c3: // BarometerInfo + $data = array( + 'BarometerInfoVersion' => substr($data, 0, 4), + 'Altitude' => getid3_lib::BigEndian2Int(substr($data, 6, 4), false, true), // m + ); + break; + } + $tag_name = (isset($NCTGtagName[$record_type]) ? $NCTGtagName[$record_type] : '0x' . str_pad(dechex($record_type), 8, '0', STR_PAD_LEFT)); + + $parsed[$tag_name] = $data; + } + } + + return $parsed; + } + + /** + * @param int $value 0x80 subtracted + * @param string $normalName 'Normal' (0 value) string + * @param string|null $format format string for numbers (default '%+d'), 3) v2 divisor + * @param int|null $div + * + * @return string + */ + protected function printPC($value, $normalName = 'Normal', $format = '%+d', $div = 1) { + switch ($value) { + case 0: + return $normalName; + case 0x7f: + return 'n/a'; + case -0x80: + return 'Auto'; + case -0x7f: + return 'User'; + } + + return sprintf($format, $value / $div); + } + + /** + * @param int|float $value + * + * @return string + */ + protected function printFraction($value) { + if (!$value) { + return '0'; + } elseif ((int) $value /$value > 0.999) { + return sprintf("%+d", (int) $value); + } elseif ((int) ($value * 2) / ($value * 2) > 0.999) { + return sprintf("%+d/2", (int) ($value * 2)); + } elseif ((int) ($value * 3) / ($value * 3) > 0.999) { + return sprintf("%+d/3", (int) ($value * 3)); + } + + return sprintf("%+.3g", $value); + } + + /** + * @param int $firstByte + * @param int $secondByte + * + * @return string + */ + protected function flashFirmwareLookup($firstByte, $secondByte) + { + $indexKey = $firstByte.' '.$secondByte; + if (isset(static::$flashInfoExternalFlashFirmwares[$indexKey])) { + return static::$flashInfoExternalFlashFirmwares[$indexKey]; + } + + return sprintf('%d.%.2d (Unknown model)', $firstByte, $secondByte); + } + + /** + * @param int $flags + * + * @return string[]|string + */ + protected function externalFlashFlagsLookup($flags) + { + $result = array(); + foreach (static::$flashInfoExternalFlashFlags as $bit => $value) { + if (($flags >> $bit) & 1) { + $result[] = $value; + } + } + + return $result; + } + + /** + * @param string $data + * @param mixed|null $serialNumber + * @param mixed|null $shutterCount + * @param int $decryptStart + * + * @return false|string + */ + protected function decryptLensInfo( + $data, + $serialNumber = null, + $shutterCount = null, + $decryptStart = 0 + ) { + if (null === $serialNumber && null === $shutterCount) { + return false; + } + + if (!is_int($serialNumber) || !is_int($shutterCount)) { + if (null !== $serialNumber && null !== $shutterCount) { + $this->getid3->warning('Invalid '.(!is_int($serialNumber) ? 'SerialNumber' : 'ShutterCount')); + } else { + $this->getid3->warning('Cannot decrypt Nikon tags because '.(null === $serialNumber ? 'SerialNumber' : 'ShutterCount').' key is not defined.'); + } + + return false; + } + + $start = $decryptStart; + $length = strlen($data) - $start; + + return $this->decrypt($data, $serialNumber, $shutterCount, $start, $length); + } + + /** + * Decrypt Nikon data block + * + * @param string $data + * @param int $serialNumber + * @param int $count + * @param int $start + * @param int $length + * + * @return string + */ + protected function decrypt($data, $serialNumber, $count, $start = 0, $length = null) + { + $maxLen = strlen($data) - $start; + if (null === $length || $length > $maxLen) { + $length = $maxLen; + } + + if ($length <= 0) { + return $data; + } + + $key = 0; + for ($i = 0; $i < 4; ++$i) { + $key ^= ($count >> ($i * 8)) & 0xFF; + } + $ci = static::$decodeTables[0][$serialNumber & 0xff]; + $cj = static::$decodeTables[1][$key]; + $ck = 0x60; + $unpackedData = array(); + for ($i = $start; $i < $length + $start; ++$i) { + $cj = ($cj + $ci * $ck) & 0xff; + $ck = ($ck + 1) & 0xff; + $unpackedData[] = ord($data[$i]) ^ $cj; + } + + $end = $start + $length; + $pre = $start ? substr($data, 0, $start) : ''; + $post = $end < strlen($data) ? substr($data, $end) : ''; + + return $pre . implode('', array_map('chr', $unpackedData)) . $post; + } + + /** + * Get serial number for use as a decryption key + * + * @param string $serialNumber + * @param string|null $model + * + * @return int|null + */ + protected function serialKey($serialNumber, $model = null) + { + if (empty($serialNumber) || ctype_digit($serialNumber)) { + return (int) $serialNumber; + } + + if (null !== $model && preg_match('#\bD50$#', $model)) { + return 0x22; + } + + return 0x60; + } +} diff --git a/lib/getid3/module.tag.xmp.php b/lib/getid3/module.tag.xmp.php index 40dd8bd..23c0d1a 100644 --- a/lib/getid3/module.tag.xmp.php +++ b/lib/getid3/module.tag.xmp.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // module.tag.xmp.php // @@ -81,8 +81,8 @@ class Image_XMP * Reads all the JPEG header segments from an JPEG image file into an array * * @param string $filename - the filename of the JPEG file to read - * @return array $headerdata - Array of JPEG header segments - * @return boolean FALSE - if headers could not be read + * @return array|false $headerdata - Array of JPEG header segments, + * FALSE - if headers could not be read */ public function _get_jpeg_header_data($filename) { @@ -114,7 +114,7 @@ class Image_XMP $data = fread($filehnd, 2); // Check that the third character is 0xFF (Start of first segment header) - if ($data{0} != "\xFF") + if ($data[0] != "\xFF") { // NO FF found - close file and return - JPEG is probably corrupted fclose($filehnd); @@ -124,15 +124,16 @@ class Image_XMP // Flag that we havent yet hit the compressed image data $hit_compressed_image_data = false; + $headerdata = array(); // Cycle through the file until, one of: 1) an EOI (End of image) marker is hit, // 2) we have hit the compressed image data (no more headers are allowed after data) // 3) or end of file is hit - while (($data{1} != "\xD9") && (!$hit_compressed_image_data) && (!feof($filehnd))) + while (($data[1] != "\xD9") && (!$hit_compressed_image_data) && (!feof($filehnd))) { // Found a segment to look at. // Check that the segment marker is not a Restart marker - restart markers don't have size or data after them - if ((ord($data{1}) < 0xD0) || (ord($data{1}) > 0xD7)) + if ((ord($data[1]) < 0xD0) || (ord($data[1]) > 0xD7)) { // Segment isn't a Restart marker // Read the next two bytes (size) @@ -149,15 +150,15 @@ class Image_XMP // Store the segment information in the output array $headerdata[] = array( - 'SegType' => ord($data{1}), - 'SegName' => $GLOBALS['JPEG_Segment_Names'][ord($data{1})], + 'SegType' => ord($data[1]), + 'SegName' => $GLOBALS['JPEG_Segment_Names'][ord($data[1])], 'SegDataStart' => $segdatastart, 'SegData' => $segdata, ); } // If this is a SOS (Start Of Scan) segment, then there is no more header data - the compressed image data follows - if ($data{1} == "\xDA") + if ($data[1] == "\xDA") { // Flag that we have hit the compressed image data - exit loop as no more headers available. $hit_compressed_image_data = true; @@ -168,7 +169,7 @@ class Image_XMP $data = fread($filehnd, 2); // Check that the first byte of the two is 0xFF as it should be for a marker - if ($data{0} != "\xFF") + if ($data[0] != "\xFF") { // NO FF found - close file and return - JPEG is probably corrupted fclose($filehnd); @@ -191,8 +192,8 @@ class Image_XMP * Retrieves XMP information from an APP1 JPEG segment and returns the raw XML text as a string. * * @param string $filename - the filename of the JPEG file to read - * @return string $xmp_data - the string of raw XML text - * @return boolean FALSE - if an APP 1 XMP segment could not be found, or if an error occured + * @return string|false $xmp_data - the string of raw XML text, + * FALSE - if an APP 1 XMP segment could not be found, or if an error occurred */ public function _get_XMP_text($filename) { @@ -200,22 +201,25 @@ class Image_XMP $jpeg_header_data = $this->_get_jpeg_header_data($filename); //Cycle through the header segments - for ($i = 0; $i < count($jpeg_header_data); $i++) - { - // If we find an APP1 header, - if (strcmp($jpeg_header_data[$i]['SegName'], 'APP1') == 0) - { - // And if it has the Adobe XMP/RDF label (http://ns.adobe.com/xap/1.0/\x00) , - if (strncmp($jpeg_header_data[$i]['SegData'], 'http://ns.adobe.com/xap/1.0/'."\x00", 29) == 0) - { - // Found a XMP/RDF block - // Return the XMP text - $xmp_data = substr($jpeg_header_data[$i]['SegData'], 29); + if (is_array($jpeg_header_data) && count($jpeg_header_data) > 0) { + foreach ($jpeg_header_data as $segment) { + // If we find an APP1 header, + if (strcmp($segment['SegName'], 'APP1') === 0) { + // And if it has the Adobe XMP/RDF label (http://ns.adobe.com/xap/1.0/\x00) , + if (strncmp($segment['SegData'], 'http://ns.adobe.com/xap/1.0/' . "\x00", 29) === 0) { + // Found a XMP/RDF block + // Return the XMP text + $xmp_data = substr($segment['SegData'], 29); - return trim($xmp_data); // trim() should not be neccesary, but some files found in the wild with null-terminated block (known samples from Apple Aperture) causes problems elsewhere (see http://www.getid3.org/phpBB3/viewtopic.php?f=4&t=1153) + // trim() should not be necessary, but some files found in the wild with null-terminated block + // (known samples from Apple Aperture) causes problems elsewhere + // (see https://www.getid3.org/phpBB3/viewtopic.php?f=4&t=1153) + return trim($xmp_data); + } } } } + return false; } @@ -223,9 +227,9 @@ class Image_XMP * Parses a string containing XMP data (XML), and returns an array * which contains all the XMP (XML) information. * - * @param string $xml_text - a string containing the XMP data (XML) to be parsed - * @return array $xmp_array - an array containing all xmp details retrieved. - * @return boolean FALSE - couldn't parse the XMP data + * @param string $xmltext - a string containing the XMP data (XML) to be parsed + * @return array|false $xmp_array - an array containing all xmp details retrieved, + * FALSE - couldn't parse the XMP data. */ public function read_XMP_array_from_text($xmltext) { @@ -304,17 +308,19 @@ class Image_XMP { // Check whether we want this details from this attribute // if (in_array($key, $GLOBALS['XMP_tag_captions'])) - if (true) - { +// if (true) +// { // Attribute wanted $xmp_array[$key] = $xml_elem['attributes'][$key]; - } +// } } } + break; case 'cdata': case 'close': break; } + break; case 'rdf:ID': case 'rdf:nodeID': @@ -362,8 +368,8 @@ class Image_XMP default: // Check whether we want the details from this attribute // if (in_array($xml_elem['tag'], $GLOBALS['XMP_tag_captions'])) - if (true) - { +// if (true) +// { switch ($xml_elem['type']) { case 'open': @@ -385,7 +391,7 @@ class Image_XMP // ignore break; } - } +// } break; } @@ -397,7 +403,7 @@ class Image_XMP /** * Constructor * - * @param string - Name of the image file to access and extract XMP information from. + * @param string $sFilename - Name of the image file to access and extract XMP information from. */ public function __construct($sFilename) { @@ -409,8 +415,11 @@ class Image_XMP $xmp_data = $this->_get_XMP_text($sFilename); if ($xmp_data) { - $this->_aXMP = $this->read_XMP_array_from_text($xmp_data); - $this->_bXMPParse = true; + $aXMP = $this->read_XMP_array_from_text($xmp_data); + if ($aXMP !== false) { + $this->_aXMP = (array) $aXMP; + $this->_bXMPParse = true; + } } } } diff --git a/lib/getid3/write.apetag.php b/lib/getid3/write.apetag.php index 6daaf3a..a34beed 100644 --- a/lib/getid3/write.apetag.php +++ b/lib/getid3/write.apetag.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.apetag.php // @@ -14,22 +14,50 @@ // /// ///////////////////////////////////////////////////////////////// - +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true); class getid3_write_apetag { - + /** + * @var string + */ public $filename; + + /** + * @var array + */ public $tag_data; - public $always_preserve_replaygain = true; // ReplayGain / MP3gain tags will be copied from old tag even if not passed in data - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * ReplayGain / MP3gain tags will be copied from old tag even if not passed in data. + * + * @var bool + */ + public $always_preserve_replaygain = true; + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteAPEtag() { // NOTE: All data passed to this function must be UTF-8 format @@ -92,6 +120,9 @@ class getid3_write_apetag return false; } + /** + * @return bool + */ public function DeleteAPEtag() { $getID3 = new getID3; $ThisFileInfo = $getID3->analyze($this->filename); @@ -125,7 +156,9 @@ class getid3_write_apetag return true; } - + /** + * @return string|false + */ public function GenerateAPEtag() { // NOTE: All data passed to this function must be UTF-8 format @@ -160,6 +193,12 @@ class getid3_write_apetag return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false); } + /** + * @param array $items + * @param bool $isheader + * + * @return string + */ public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) { $tagdatalength = 0; foreach ($items as $itemdata) { @@ -176,6 +215,15 @@ class getid3_write_apetag return $APEheader; } + /** + * @param bool $header + * @param bool $footer + * @param bool $isheader + * @param int $encodingid + * @param bool $readonly + * + * @return string + */ public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) { $APEtagFlags = array_fill(0, 4, 0); if ($header) { @@ -201,6 +249,11 @@ class getid3_write_apetag return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]); } + /** + * @param string $itemkey + * + * @return string + */ public function CleanAPEtagItemKey($itemkey) { $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey); diff --git a/lib/getid3/write.id3v1.php b/lib/getid3/write.id3v1.php index 387abe7..c961e93 100644 --- a/lib/getid3/write.id3v1.php +++ b/lib/getid3/write.id3v1.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.id3v1.php // @@ -14,20 +14,48 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v1.php', __FILE__, true); class getid3_write_id3v1 { + /** + * @var string + */ public $filename; + + /** + * @var int + */ public $filesize; + + /** + * @var array + */ public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteID3v1() { // File MUST be writeable - CHMOD(646) at least if (!empty($this->filename) && is_readable($this->filename) && getID3::is_writable($this->filename) && is_file($this->filename)) { @@ -43,16 +71,17 @@ class getid3_write_id3v1 } else { fseek($fp_source, 0, SEEK_END); // append new ID3v1 tag } - $this->tag_data['track'] = (isset($this->tag_data['track']) ? $this->tag_data['track'] : (isset($this->tag_data['track_number']) ? $this->tag_data['track_number'] : (isset($this->tag_data['tracknumber']) ? $this->tag_data['tracknumber'] : ''))); + $this->tag_data['track_number'] = (isset($this->tag_data['track_number']) ? $this->tag_data['track_number'] : ''); $new_id3v1_tag_data = getid3_id3v1::GenerateID3v1Tag( - (isset($this->tag_data['title'] ) ? $this->tag_data['title'] : ''), - (isset($this->tag_data['artist'] ) ? $this->tag_data['artist'] : ''), - (isset($this->tag_data['album'] ) ? $this->tag_data['album'] : ''), - (isset($this->tag_data['year'] ) ? $this->tag_data['year'] : ''), - (isset($this->tag_data['genreid']) ? $this->tag_data['genreid'] : ''), - (isset($this->tag_data['comment']) ? $this->tag_data['comment'] : ''), - (isset($this->tag_data['track'] ) ? $this->tag_data['track'] : '')); + (isset($this->tag_data['title'] ) ? $this->tag_data['title'] : ''), + (isset($this->tag_data['artist'] ) ? $this->tag_data['artist'] : ''), + (isset($this->tag_data['album'] ) ? $this->tag_data['album'] : ''), + (isset($this->tag_data['year'] ) ? $this->tag_data['year'] : ''), + (isset($this->tag_data['genreid'] ) ? $this->tag_data['genreid'] : ''), + (isset($this->tag_data['comment'] ) ? $this->tag_data['comment'] : ''), + $this->tag_data['track_number'] + ); fwrite($fp_source, $new_id3v1_tag_data, 128); fclose($fp_source); return true; @@ -66,6 +95,9 @@ class getid3_write_id3v1 return false; } + /** + * @return bool + */ public function FixID3v1Padding() { // ID3v1 data is supposed to be padded with NULL characters, but some taggers incorrectly use spaces // This function rewrites the ID3v1 tag with correct padding @@ -79,6 +111,7 @@ class getid3_write_id3v1 $getID3->option_tag_id3v1 = true; $ThisFileInfo = $getID3->analyze($this->filename); if (isset($ThisFileInfo['tags']['id3v1'])) { + $id3v1data = array(); foreach ($ThisFileInfo['tags']['id3v1'] as $key => $value) { $id3v1data[$key] = implode(',', $value); } @@ -88,6 +121,9 @@ class getid3_write_id3v1 return false; } + /** + * @return bool + */ public function RemoveID3v1() { // File MUST be writeable - CHMOD(646) at least if (!empty($this->filename) && is_readable($this->filename) && getID3::is_writable($this->filename) && is_file($this->filename)) { @@ -116,6 +152,9 @@ class getid3_write_id3v1 return false; } + /** + * @return bool + */ public function setRealFileSize() { if (PHP_INT_MAX > 2147483647) { $this->filesize = filesize($this->filename); diff --git a/lib/getid3/write.id3v2.php b/lib/getid3/write.id3v2.php index cc5c8a1..e6b40f3 100644 --- a/lib/getid3/write.id3v2.php +++ b/lib/getid3/write.id3v2.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// /// // // write.id3v2.php // @@ -14,26 +14,93 @@ // /// ///////////////////////////////////////////////////////////////// +if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers + exit; +} getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); class getid3_write_id3v2 { + /** + * @var string + */ public $filename; + + /** + * @var array|null + */ public $tag_data; - public $fread_buffer_size = 32768; // read buffer size in bytes - public $paddedlength = 4096; // minimum length of ID3v2 tag in bytes - public $majorversion = 3; // ID3v2 major version (2, 3 (recommended), 4) - public $minorversion = 0; // ID3v2 minor version - always 0 - public $merge_existing_data = false; // if true, merge new data with existing tags; if false, delete old tag data and only write new tags - public $id3v2_default_encodingid = 0; // default text encoding (ISO-8859-1) if not explicitly passed - public $id3v2_use_unsynchronisation = false; // the specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used, so by default don't use it. - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * Read buffer size in bytes. + * + * @var int + */ + public $fread_buffer_size = 32768; + + /** + * Minimum length of ID3v2 tag in bytes. + * + * @var int + */ + public $paddedlength = 4096; + + /** + * ID3v2 major version (2, 3 (recommended), 4). + * + * @var int + */ + public $majorversion = 3; + + /** + * ID3v2 minor version - always 0. + * + * @var int + */ + public $minorversion = 0; + + /** + * If true, merge new data with existing tags; if false, delete old tag data and only write new tags. + * + * @var bool + */ + public $merge_existing_data = false; + + /** + * Default text encoding (ISO-8859-1) if not explicitly passed. + * + * @var int + */ + public $id3v2_default_encodingid = 0; + + /** + * The specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used, + * so by default don't use it. + * + * @var bool + */ + public $id3v2_use_unsynchronisation = false; + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteID3v2() { // File MUST be writeable - CHMOD(646) at least. It's best if the // directory is also writeable, because that method is both faster and less susceptible to errors. @@ -44,7 +111,6 @@ class getid3_write_id3v2 $OldThisFileInfo = $getID3->analyze($this->filename); if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) { $this->errors[] = 'Unable to write ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - fclose($fp_source); return false; } if ($this->merge_existing_data) { @@ -60,26 +126,12 @@ class getid3_write_id3v2 if (file_exists($this->filename) && getID3::is_writable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) { // best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary) - if (file_exists($this->filename)) { - - if (is_readable($this->filename) && getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; - } - + if (is_readable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) { + rewind($fp); + fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); + fclose($fp); } else { - - if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")'; - } - + $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; } } else { @@ -134,6 +186,9 @@ class getid3_write_id3v2 return false; } + /** + * @return bool + */ public function RemoveID3v2() { // File MUST be writeable - CHMOD(646) at least. It's best if the // directory is also writeable, because that method is both faster and less susceptible to errors. @@ -155,7 +210,7 @@ class getid3_write_id3v2 if ($OldThisFileInfo['avdataoffset'] !== false) { fseek($fp_source, $OldThisFileInfo['avdataoffset']); } - if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) { + if (getID3::is_writable($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) { while ($buffer = fread($fp_source, $this->fread_buffer_size)) { fwrite($fp_temp, $buffer, strlen($buffer)); } @@ -195,7 +250,7 @@ class getid3_write_id3v2 fwrite($fp_temp, $buffer, strlen($buffer)); } fclose($fp_source); - if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { + if (getID3::is_writable($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { rewind($fp_temp); while ($buffer = fread($fp_temp, $this->fread_buffer_size)) { fwrite($fp_source, $buffer, strlen($buffer)); @@ -225,8 +280,13 @@ class getid3_write_id3v2 return true; } - + /** + * @param array $flags + * + * @return string|false + */ public function GenerateID3v2TagFlags($flags) { + $flag = null; switch ($this->majorversion) { case 4: // %abcd0000 @@ -254,13 +314,25 @@ class getid3_write_id3v2 default: return false; - break; } return chr(bindec($flag)); } - + /** + * @param bool $TagAlter + * @param bool $FileAlter + * @param bool $ReadOnly + * @param bool $Compression + * @param bool $Encryption + * @param bool $GroupingIdentity + * @param bool $Unsynchronisation + * @param bool $DataLengthIndicator + * + * @return string|false + */ public function GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false) { + $flag1 = null; + $flag2 = null; switch ($this->majorversion) { case 4: // %0abc0000 %0h00kmnp @@ -294,12 +366,17 @@ class getid3_write_id3v2 default: return false; - break; } return chr(bindec($flag1)).chr(bindec($flag2)); } + /** + * @param string $frame_name + * @param array $source_data_array + * + * @return string|false + */ public function GenerateID3v2FrameData($frame_name, $source_data_array) { if (!getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) { return false; @@ -331,7 +408,7 @@ class getid3_write_id3v2 // Description $00 (00) // Value $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { + if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; } else { $framedata .= chr($source_data_array['encodingid']); @@ -346,9 +423,9 @@ class getid3_write_id3v2 // Description $00 (00) // URL $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { + if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false, false)) { + } elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false)) { //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; // probably should be an error, need to rewrite IsValidURL() to handle other encodings $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; @@ -364,7 +441,7 @@ class getid3_write_id3v2 // Text encoding $xx // People list strings $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { + if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; } else { $framedata .= chr($source_data_array['encodingid']); @@ -397,13 +474,14 @@ class getid3_write_id3v2 if (!$this->ID3v2IsValidETCOevent($val['typeid'])) { $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')'; } elseif (($key != 'timestampformat') && ($key != 'flags')) { - if (($val['timestamp'] > 0) && ($previousETCOtimestamp >= $val['timestamp'])) { + if (($val['timestamp'] > 0) && isset($previousETCOtimestamp) && ($previousETCOtimestamp >= $val['timestamp'])) { // The 'Time stamp' is set to zero if directly at the beginning of the sound // or after the previous event. All events MUST be sorted in chronological order. $this->errors[] = 'Out-of-order timestamp in '.$frame_name.' ('.$val['timestamp'].') for Event Type ('.$val['typeid'].')'; } else { $framedata .= chr($val['typeid']); $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false); + $previousETCOtimestamp = $val['timestamp']; } } } @@ -453,6 +531,7 @@ class getid3_write_id3v2 } else { $this->errors[] = 'Invalid Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsformsdeviation'].')'; } + $unwrittenbitstream = ''; foreach ($source_data_array as $key => $val) { if (($key != 'framesbetweenreferences') && ($key != 'bytesbetweenreferences') && ($key != 'msbetweenreferences') && ($key != 'bitsforbytesdeviation') && ($key != 'bitsformsdeviation') && ($key != 'flags')) { $unwrittenbitstream .= str_pad(getid3_lib::Dec2Bin($val['bytedeviation']), $source_data_array['bitsforbytesdeviation'], '0', STR_PAD_LEFT); @@ -617,7 +696,7 @@ class getid3_write_id3v2 if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) { $this->errors[] = 'Invalid Bits For Volume Description byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)'; } else { - $incdecflag .= '00'; + $incdecflag = '00'; $incdecflag .= $source_data_array['incdec']['right'] ? '1' : '0'; // a - Relative volume change, right $incdecflag .= $source_data_array['incdec']['left'] ? '1' : '0'; // b - Relative volume change, left $incdecflag .= $source_data_array['incdec']['rightrear'] ? '1' : '0'; // c - Relative volume change, right back @@ -760,9 +839,9 @@ class getid3_write_id3v2 $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; } elseif (!$this->ID3v2IsValidAPICpicturetype($source_data_array['picturetypeid'])) { $this->errors[] = 'Invalid Picture Type byte in '.$frame_name.' ('.$source_data_array['picturetypeid'].') for ID3v2.'.$this->majorversion; - } elseif (($this->majorversion >= 3) && (!$this->ID3v2IsValidAPICimageformat($source_data_array['mime']))) { + } elseif ((!$this->ID3v2IsValidAPICimageformat($source_data_array['mime']))) { $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].') for ID3v2.'.$this->majorversion; - } elseif (($source_data_array['mime'] == '-->') && (!$this->IsValidURL($source_data_array['data'], false, false))) { + } elseif (($source_data_array['mime'] == '-->') && (!$this->IsValidURL($source_data_array['data'], false))) { //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; // probably should be an error, need to rewrite IsValidURL() to handle other encodings $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; @@ -813,10 +892,14 @@ class getid3_write_id3v2 // Email to user $00 // Rating $xx // Counter $xx xx xx xx (xx ...) + if (!$this->IsValidEmail($source_data_array['email'])) { + // https://github.com/JamesHeinrich/getID3/issues/216 + // https://en.wikipedia.org/wiki/ID3#ID3v2_rating_tag_issue + // ID3v2 specs say it should be an email address, but Windows instead uses string like "Windows Media Player 9 Series" + $this->warnings[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')'; + } if (!$this->IsWithinBitRange($source_data_array['rating'], 8, false)) { $this->errors[] = 'Invalid Rating byte in '.$frame_name.' ('.$source_data_array['rating'].') (range = 0 to 255)'; - } elseif (!$this->IsValidEmail($source_data_array['email'])) { - $this->errors[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')'; } else { $framedata .= str_replace("\x00", '', $source_data_array['email'])."\x00"; $framedata .= chr($source_data_array['rating']); @@ -835,7 +918,7 @@ class getid3_write_id3v2 $this->errors[] = 'Invalid Offset To Next Tag in '.$frame_name; } else { $framedata .= getid3_lib::BigEndian2String($source_data_array['buffersize'], 3, false); - $flag .= '0000000'; + $flag = '0000000'; $flag .= $source_data_array['flags']['embededinfo'] ? '1' : '0'; $framedata .= chr(bindec($flag)); $framedata .= getid3_lib::BigEndian2String($source_data_array['nexttagoffset'], 4, false); @@ -867,7 +950,7 @@ class getid3_write_id3v2 // ID and additional data if (!getid3_id3v2::IsValidID3v2FrameName($source_data_array['frameid'], $this->majorversion)) { $this->errors[] = 'Invalid Frame Identifier in '.$frame_name.' ('.$source_data_array['frameid'].')'; - } elseif (!$this->IsValidURL($source_data_array['data'], true, false)) { + } elseif (!$this->IsValidURL($source_data_array['data'], true)) { //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; // probably should be an error, need to rewrite IsValidURL() to handle other encodings $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; @@ -914,6 +997,7 @@ class getid3_write_id3v2 } else { $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')'; } + break; default: if ((substr($source_data_array['frameid'], 0, 1) == 'T') || (substr($source_data_array['frameid'], 0, 1) == 'W')) { @@ -966,9 +1050,9 @@ class getid3_write_id3v2 $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')'; - } elseif (!$this->IsANumber($source_data_array['pricepaid']['value'], false)) { + } elseif (!getid3_id3v2::IsANumber($source_data_array['pricepaid']['value'], false)) { $this->errors[] = 'Invalid Price Paid in '.$frame_name.' ('.$source_data_array['pricepaid']['value'].')'; - } elseif (!$this->IsValidDateStampString($source_data_array['purchasedate'])) { + } elseif (!getid3_id3v2::IsValidDateStampString($source_data_array['purchasedate'])) { $this->errors[] = 'Invalid Date Of Purchase in '.$frame_name.' ('.$source_data_array['purchasedate'].') (format = YYYYMMDD)'; } else { $framedata .= chr($source_data_array['encodingid']); @@ -992,9 +1076,9 @@ class getid3_write_id3v2 $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')'; - } elseif (!$this->IsValidDateStampString($source_data_array['pricevaliduntil'])) { + } elseif (!getid3_id3v2::IsValidDateStampString($source_data_array['pricevaliduntil'])) { $this->errors[] = 'Invalid Valid Until date in '.$frame_name.' ('.$source_data_array['pricevaliduntil'].') (format = YYYYMMDD)'; - } elseif (!$this->IsValidURL($source_data_array['contacturl'], false, true)) { + } elseif (!$this->IsValidURL($source_data_array['contacturl'], false)) { $this->errors[] = 'Invalid Contact URL in '.$frame_name.' ('.$source_data_array['contacturl'].') (allowed schemes: http, https, ftp, mailto)'; } elseif (!$this->ID3v2IsValidCOMRreceivedAs($source_data_array['receivedasid'])) { $this->errors[] = 'Invalid Received As byte in '.$frame_name.' ('.$source_data_array['contacturl'].') (range = 0 to 8)'; @@ -1002,7 +1086,7 @@ class getid3_write_id3v2 $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')'; } else { $framedata .= chr($source_data_array['encodingid']); - unset($pricestring); + $pricestrings = array(); foreach ($source_data_array['price'] as $key => $val) { if ($this->ID3v2IsValidPriceString($key.$val['value'])) { $pricestrings[] = $key.$val['value']; @@ -1139,9 +1223,9 @@ class getid3_write_id3v2 break; default: - if ((($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (strlen($frame_name) != 4))) { + if (/*(($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (*/strlen($frame_name) != 4/*))*/) { $this->errors[] = 'Invalid frame name "'.$frame_name.'" for ID3v2.'.$this->majorversion; - } elseif ($frame_name{0} == 'T') { + } elseif ($frame_name[0] == 'T') { // 4.2. T??? Text information frames // Text encoding $xx // Information @@ -1152,10 +1236,10 @@ class getid3_write_id3v2 $framedata .= chr($source_data_array['encodingid']); $framedata .= $source_data_array['data']; } - } elseif ($frame_name{0} == 'W') { + } elseif ($frame_name[0] == 'W') { // 4.3. W??? URL link frames // URL - if (!$this->IsValidURL($source_data_array['data'], false, false)) { + if (!$this->IsValidURL($source_data_array['data'], false)) { //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; // probably should be an error, need to rewrite IsValidURL() to handle other encodings $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; @@ -1174,6 +1258,12 @@ class getid3_write_id3v2 return $framedata; } + /** + * @param string|null $frame_name + * @param array $source_data_array + * + * @return bool + */ public function ID3v2FrameIsAllowed($frame_name, $source_data_array) { static $PreviousFrames = array(); @@ -1302,7 +1392,7 @@ class getid3_write_id3v2 break; default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { + if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) { $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; } break; @@ -1425,7 +1515,7 @@ class getid3_write_id3v2 break; default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { + if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) { $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; } break; @@ -1517,7 +1607,7 @@ class getid3_write_id3v2 break; default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { + if (($frame_name[0] != 'T') && ($frame_name[0] != 'W')) { $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; } break; @@ -1530,6 +1620,11 @@ class getid3_write_id3v2 return true; } + /** + * @param bool $noerrorsonly + * + * @return string|false + */ public function GenerateID3v2Tag($noerrorsonly=true) { $this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag() @@ -1583,18 +1678,18 @@ class getid3_write_id3v2 if ($noerrorsonly) { return false; } else { - unset($frame_name); + $frame_name = null; } } } else { // ignore any invalid frame names, including 'title', 'header', etc $this->warnings[] = 'Ignoring invalid ID3v2 frame type: "'.$frame_name.'"'; - unset($frame_name); + $frame_name = null; unset($frame_length); unset($frame_flags); unset($frame_data); } - if (isset($frame_name) && isset($frame_length) && isset($frame_flags) && isset($frame_data)) { + if (null !== $frame_name && isset($frame_length) && isset($frame_flags) && isset($frame_data)) { $tagstring .= $frame_name.$frame_length.$frame_flags.$frame_data; } } @@ -1618,7 +1713,7 @@ class getid3_write_id3v2 } $footer = false; // ID3v2 footers not yet supported in getID3() - if (!$footer && ($this->paddedlength > (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)))) { + if (/*!$footer && */($this->paddedlength > (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)))) { // pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength // "Furthermore it MUST NOT have any padding when a tag footer is added to the tag." if (($this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)) > 0) { @@ -1644,20 +1739,31 @@ class getid3_write_id3v2 return false; } + /** + * @param string $pricestring + * + * @return bool + */ public function ID3v2IsValidPriceString($pricestring) { if (getid3_id3v2::LanguageLookup(substr($pricestring, 0, 3), true) == '') { return false; - } elseif (!$this->IsANumber(substr($pricestring, 3), true)) { + } elseif (!getid3_id3v2::IsANumber(substr($pricestring, 3), true)) { return false; } return true; } + /** + * @param string $framename + * + * @return bool + */ public function ID3v2FrameFlagsLookupTagAlter($framename) { // unfinished switch ($framename) { case 'RGAD': $allow = true; + break; default: $allow = false; break; @@ -1665,19 +1771,27 @@ class getid3_write_id3v2 return $allow; } + /** + * @param string $framename + * + * @return bool + */ public function ID3v2FrameFlagsLookupFileAlter($framename) { // unfinished switch ($framename) { case 'RGAD': return false; - break; default: return false; - break; } } + /** + * @param int $eventid + * + * @return bool + */ public function ID3v2IsValidETCOevent($eventid) { if (($eventid < 0) || ($eventid > 0xFF)) { // outside range of 1 byte @@ -1698,6 +1812,11 @@ class getid3_write_id3v2 return true; } + /** + * @param int $contenttype + * + * @return bool + */ public function ID3v2IsValidSYLTtype($contenttype) { if (($contenttype >= 0) && ($contenttype <= 8) && ($this->majorversion == 4)) { return true; @@ -1707,6 +1826,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $channeltype + * + * @return bool + */ public function ID3v2IsValidRVA2channeltype($channeltype) { if (($channeltype >= 0) && ($channeltype <= 8) && ($this->majorversion == 4)) { return true; @@ -1714,6 +1838,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $picturetype + * + * @return bool + */ public function ID3v2IsValidAPICpicturetype($picturetype) { if (($picturetype >= 0) && ($picturetype <= 0x14) && ($this->majorversion >= 2) && ($this->majorversion <= 4)) { return true; @@ -1721,6 +1850,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int|string $imageformat + * + * @return bool + */ public function ID3v2IsValidAPICimageformat($imageformat) { if ($imageformat == '-->') { return true; @@ -1736,6 +1870,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $receivedas + * + * @return bool + */ public function ID3v2IsValidCOMRreceivedAs($receivedas) { if (($this->majorversion >= 3) && ($receivedas >= 0) && ($receivedas <= 8)) { return true; @@ -1743,6 +1882,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $RGADname + * + * @return bool + */ public static function ID3v2IsValidRGADname($RGADname) { if (($RGADname >= 0) && ($RGADname <= 2)) { return true; @@ -1750,6 +1894,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $RGADoriginator + * + * @return bool + */ public static function ID3v2IsValidRGADoriginator($RGADoriginator) { if (($RGADoriginator >= 0) && ($RGADoriginator <= 3)) { return true; @@ -1757,6 +1906,11 @@ class getid3_write_id3v2 return false; } + /** + * @param int $textencodingbyte + * + * @return bool + */ public function ID3v2IsValidTextEncoding($textencodingbyte) { // 0 = ISO-8859-1 // 1 = UTF-16 with BOM @@ -1770,6 +1924,11 @@ class getid3_write_id3v2 return isset($ID3v2IsValidTextEncoding_cache[$this->majorversion][$textencodingbyte]); } + /** + * @param string $data + * + * @return string + */ public static function Unsynchronise($data) { // Whenever a false synchronisation is found within the tag, one zeroed // byte is inserted after the first false synchronisation byte. The @@ -1787,10 +1946,10 @@ class getid3_write_id3v2 $unsyncheddata = ''; $datalength = strlen($data); for ($i = 0; $i < $datalength; $i++) { - $thischar = $data{$i}; + $thischar = $data[$i]; $unsyncheddata .= $thischar; if ($thischar == "\xFF") { - $nextchar = ord($data{$i + 1}); + $nextchar = ord($data[$i + 1]); if (($nextchar & 0xE0) == 0xE0) { // previous byte = 11111111, this byte = 111????? $unsyncheddata .= "\x00"; @@ -1800,14 +1959,19 @@ class getid3_write_id3v2 return $unsyncheddata; } + /** + * @param mixed $var + * + * @return bool + */ public function is_hash($var) { // written by dev-nullØchristophe*vg // taken from http://www.php.net/manual/en/function.array-merge-recursive.php if (is_array($var)) { $keys = array_keys($var); $all_num = true; - for ($i = 0; $i < count($keys); $i++) { - if (is_string($keys[$i])) { + foreach ($keys as $key) { + if (is_string($key)) { return true; } } @@ -1815,6 +1979,12 @@ class getid3_write_id3v2 return false; } + /** + * @param mixed $arr1 + * @param mixed $arr2 + * + * @return array + */ public function array_join_merge($arr1, $arr2) { // written by dev-nullØchristophe*vg // taken from http://www.php.net/manual/en/function.array-merge-recursive.php @@ -1839,10 +2009,22 @@ class getid3_write_id3v2 } } + /** + * @param string $mimestring + * + * @return false|int + */ public static function IsValidMIMEstring($mimestring) { return preg_match('#^.+/.+$#', $mimestring); } + /** + * @param int $number + * @param int $maxbits + * @param bool $signed + * + * @return bool + */ public static function IsWithinBitRange($number, $maxbits, $signed=false) { if ($signed) { if (($number > (0 - pow(2, $maxbits - 1))) && ($number <= pow(2, $maxbits - 1))) { @@ -1856,6 +2038,11 @@ class getid3_write_id3v2 return false; } + /** + * @param string $email + * + * @return false|int|mixed + */ public static function IsValidEmail($email) { if (function_exists('filter_var')) { return filter_var($email, FILTER_VALIDATE_EMAIL); @@ -1864,6 +2051,12 @@ class getid3_write_id3v2 return preg_match('#^[^ ]+@[a-z\\-\\.]+\\.[a-z]{2,}$#', $email); } + /** + * @param string $url + * @param bool $allowUserPass + * + * @return bool + */ public static function IsValidURL($url, $allowUserPass=false) { if ($url == '') { return false; @@ -1876,7 +2069,7 @@ class getid3_write_id3v2 } } // 2016-06-08: relax URL checking to avoid falsely rejecting valid URLs, leave URL validation to the user - // http://www.getid3.org/phpBB3/viewtopic.php?t=1926 + // https://www.getid3.org/phpBB3/viewtopic.php?t=1926 return true; /* if ($parts = $this->safe_parse_url($url)) { @@ -1900,6 +2093,11 @@ class getid3_write_id3v2 */ } + /** + * @param string $url + * + * @return array + */ public static function safe_parse_url($url) { $parts = @parse_url($url); $parts['scheme'] = (isset($parts['scheme']) ? $parts['scheme'] : ''); @@ -1911,6 +2109,12 @@ class getid3_write_id3v2 return $parts; } + /** + * @param int $majorversion + * @param string $long_description + * + * @return string + */ public static function ID3v2ShortFrameNameLookup($majorversion, $long_description) { $long_description = str_replace(' ', '_', strtolower(trim($long_description))); static $ID3v2ShortFrameNameLookup = array(); @@ -1979,7 +2183,7 @@ class getid3_write_id3v2 $ID3v2ShortFrameNameLookup[2]['text'] = 'TXX'; $ID3v2ShortFrameNameLookup[2]['year'] = 'TYE'; $ID3v2ShortFrameNameLookup[2]['unique_file_identifier'] = 'UFI'; - $ID3v2ShortFrameNameLookup[2]['unsychronised_lyric'] = 'ULT'; + $ID3v2ShortFrameNameLookup[2]['unsynchronised_lyric'] = 'ULT'; $ID3v2ShortFrameNameLookup[2]['url_file'] = 'WAF'; $ID3v2ShortFrameNameLookup[2]['url_artist'] = 'WAR'; $ID3v2ShortFrameNameLookup[2]['url_source'] = 'WAS'; @@ -2054,7 +2258,7 @@ class getid3_write_id3v2 $ID3v2ShortFrameNameLookup[3]['text'] = 'TXXX'; $ID3v2ShortFrameNameLookup[3]['unique_file_identifier'] = 'UFID'; $ID3v2ShortFrameNameLookup[3]['terms_of_use'] = 'USER'; - $ID3v2ShortFrameNameLookup[3]['unsychronised_lyric'] = 'USLT'; + $ID3v2ShortFrameNameLookup[3]['unsynchronised_lyric'] = 'USLT'; $ID3v2ShortFrameNameLookup[3]['commercial_information'] = 'WCOM'; $ID3v2ShortFrameNameLookup[3]['copyright'] = 'WCOP'; $ID3v2ShortFrameNameLookup[3]['url_file'] = 'WOAF'; @@ -2100,6 +2304,7 @@ class getid3_write_id3v2 $ID3v2ShortFrameNameLookup[4]['performer_sort_order'] = 'TSOP'; $ID3v2ShortFrameNameLookup[4]['title_sort_order'] = 'TSOT'; $ID3v2ShortFrameNameLookup[4]['set_subtitle'] = 'TSST'; + $ID3v2ShortFrameNameLookup[4]['year'] = 'TDRC'; // subset of ISO 8601: valid timestamps are yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and yyyy-MM-ddTHH:mm:ss. All time stamps are UTC } return (isset($ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)]) ? $ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)] : ''); diff --git a/lib/getid3/write.lyrics3.php b/lib/getid3/write.lyrics3.php index 06fc943..4c5c868 100644 --- a/lib/getid3/write.lyrics3.php +++ b/lib/getid3/write.lyrics3.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.lyrics3.php // @@ -17,20 +17,45 @@ class getid3_write_lyrics3 { + /** + * @var string + */ public $filename; + + /** + * @var array + */ public $tag_data; //public $lyrics3_version = 2; // 1 or 2 - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteLyrics3() { $this->errors[] = 'WriteLyrics3() not yet functional - cannot write Lyrics3'; return false; } + + /** + * @return bool + */ public function DeleteLyrics3() { // Initialize getID3 engine $getID3 = new getID3; diff --git a/lib/getid3/write.metaflac.php b/lib/getid3/write.metaflac.php index 8c2b75d..65cf3c1 100644 --- a/lib/getid3/write.metaflac.php +++ b/lib/getid3/write.metaflac.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.metaflac.php // @@ -17,16 +17,38 @@ class getid3_write_metaflac { - + /** + * @var string + */ public $filename; + + /** + * @var array + */ public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); + + private $pictures = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteMetaFLAC() { if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { @@ -34,8 +56,34 @@ class getid3_write_metaflac return false; } + $tempfilenames = array(); + + + if (!empty($this->tag_data['ATTACHED_PICTURE'])) { + foreach ($this->tag_data['ATTACHED_PICTURE'] as $key => $picturedetails) { + $temppicturefilename = tempnam(GETID3_TEMP_DIR, 'getID3'); + $tempfilenames[] = $temppicturefilename; + if (getID3::is_writable($temppicturefilename) && is_file($temppicturefilename) && ($fpcomments = fopen($temppicturefilename, 'wb'))) { + // https://xiph.org/flac/documentation_tools_flac.html#flac_options_picture + // [TYPE]|[MIME-TYPE]|[DESCRIPTION]|[WIDTHxHEIGHTxDEPTH[/COLORS]]|FILE + fwrite($fpcomments, $picturedetails['data']); + fclose($fpcomments); + $picture_typeid = (!empty($picturedetails['picturetypeid']) ? $this->ID3v2toFLACpictureTypes($picturedetails['picturetypeid']) : 3); // default to "3:Cover (front)" + $picture_mimetype = (!empty($picturedetails['mime']) ? $picturedetails['mime'] : ''); // should be auto-detected + $picture_width_height_depth = ''; + $this->pictures[] = $picture_typeid.'|'.$picture_mimetype.'|'.preg_replace('#[^\x20-\x7B\x7D-\x7F]#', '', $picturedetails['description']).'|'.$picture_width_height_depth.'|'.$temppicturefilename; + } else { + $this->errors[] = 'failed to open temporary tags file, tags not written - fopen("'.$temppicturefilename.'", "wb")'; + return false; + } + } + unset($this->tag_data['ATTACHED_PICTURE']); + } + + // Create file with new comments $tempcommentsfilename = tempnam(GETID3_TEMP_DIR, 'getID3'); + $tempfilenames[] = $tempcommentsfilename; if (getID3::is_writable($tempcommentsfilename) && is_file($tempcommentsfilename) && ($fpcomments = fopen($tempcommentsfilename, 'wb'))) { foreach ($this->tag_data as $key => $value) { foreach ($value as $commentdata) { @@ -62,14 +110,18 @@ class getid3_write_metaflac // On top of that, if error messages are not always captured properly under Windows // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); + clearstatcache(true, $this->filename); $timestampbeforewriting = filemtime($this->filename); - $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename).' '.escapeshellarg($this->filename).' 2>&1'; + $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename); + foreach ($this->pictures as $picturecommand) { + $commandline .= ' --import-picture-from='.escapeshellarg($picturecommand); + } + $commandline .= ' '.escapeshellarg($this->filename).' 2>&1'; $metaflacError = `$commandline`; if (empty($metaflacError)) { - clearstatcache(); + clearstatcache(true, $this->filename); if ($timestampbeforewriting == filemtime($this->filename)) { $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not written'; } @@ -81,13 +133,19 @@ class getid3_write_metaflac } else { // It's simpler on *nix - $commandline = 'metaflac --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename).' '.escapeshellarg($this->filename).' 2>&1'; + $commandline = 'metaflac --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename); + foreach ($this->pictures as $picturecommand) { + $commandline .= ' --import-picture-from='.escapeshellarg($picturecommand); + } + $commandline .= ' '.escapeshellarg($this->filename).' 2>&1'; $metaflacError = `$commandline`; } // Remove temporary comments file - unlink($tempcommentsfilename); + foreach ($tempfilenames as $tempfilename) { + unlink($tempfilename); + } ignore_user_abort($oldignoreuserabort); if (!empty($metaflacError)) { @@ -100,7 +158,9 @@ class getid3_write_metaflac return true; } - + /** + * @return bool + */ public function DeleteMetaFLAC() { if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { @@ -113,14 +173,14 @@ class getid3_write_metaflac if (file_exists(GETID3_HELPERAPPSDIR.'metaflac.exe')) { // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); + clearstatcache(true, $this->filename); $timestampbeforewriting = filemtime($this->filename); $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --remove-all-tags "'.$this->filename.'" 2>&1'; $metaflacError = `$commandline`; if (empty($metaflacError)) { - clearstatcache(); + clearstatcache(true, $this->filename); if ($timestampbeforewriting == filemtime($this->filename)) { $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not deleted'; } @@ -146,7 +206,24 @@ class getid3_write_metaflac return true; } + /** + * @param int $id3v2_picture_typeid + * + * @return int + */ + public function ID3v2toFLACpictureTypes($id3v2_picture_typeid) { + // METAFLAC picture type list is identical to ID3v2 picture type list (as least up to 0x14 "Publisher/Studio logotype") + // http://id3.org/id3v2.4.0-frames (section 4.14) + // https://xiph.org/flac/documentation_tools_flac.html#flac_options_picture + //return (isset($ID3v2toFLACpictureTypes[$id3v2_picture_typeid]) ? $ID3v2toFLACpictureTypes[$id3v2_picture_typeid] : 3); // default: "3: Cover (front)" + return (($id3v2_picture_typeid <= 0x14) ? $id3v2_picture_typeid : 3); // default: "3: Cover (front)" + } + /** + * @param string $originalcommentname + * + * @return string + */ public function CleanmetaflacName($originalcommentname) { // A case-insensitive field name that may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded. // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to ASCII 0x61 through @@ -156,7 +233,6 @@ class getid3_write_metaflac // Thanks Chris Bolt for improving this function // note: *reg_replace() replaces nulls with empty string (not space) return strtoupper(preg_replace('#[^ -<>-}]#', ' ', str_replace("\x00", ' ', $originalcommentname))); - } } diff --git a/lib/getid3/write.php b/lib/getid3/write.php index 4f2d851..ea28567 100644 --- a/lib/getid3/write.php +++ b/lib/getid3/write.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// /// // // write.php // @@ -27,48 +27,113 @@ if (!include_once(GETID3_INCLUDEPATH.'getid3.lib.php')) { throw new Exception('write.php depends on getid3.lib.php, which is missing.'); } - -// NOTES: -// -// You should pass data here with standard field names as follows: -// * TITLE -// * ARTIST -// * ALBUM -// * TRACKNUMBER -// * COMMENT -// * GENRE -// * YEAR -// * ATTACHED_PICTURE (ID3v2 only) -// -// http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html -// The APEv2 Tag Items Keys definition says "TRACK" is correct but foobar2000 uses "TRACKNUMBER" instead -// Pass data here as "TRACKNUMBER" for compatability with all formats - - +/** + * NOTES: + * + * You should pass data here with standard field names as follows: + * * TITLE + * * ARTIST + * * ALBUM + * * TRACKNUMBER + * * COMMENT + * * GENRE + * * YEAR + * * ATTACHED_PICTURE (ID3v2 only) + * The APEv2 Tag Items Keys definition says "TRACK" is correct but foobar2000 uses "TRACKNUMBER" instead + * Pass data here as "TRACKNUMBER" for compatability with all formats + * + * @link http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html + */ class getid3_writetags { - // public - public $filename; // absolute filename of file to write tags to - public $tagformats = array(); // array of tag formats to write ('id3v1', 'id3v2.2', 'id2v2.3', 'id3v2.4', 'ape', 'vorbiscomment', 'metaflac', 'real') - public $tag_data = array(array()); // 2-dimensional array of tag data (ex: $data['ARTIST'][0] = 'Elvis') - public $tag_encoding = 'ISO-8859-1'; // text encoding used for tag data ('ISO-8859-1', 'UTF-8', 'UTF-16', 'UTF-16LE', 'UTF-16BE', ) - public $overwrite_tags = true; // if true will erase existing tag data and write only passed data; if false will merge passed data with existing tag data - public $remove_other_tags = false; // if true will erase remove all existing tags and only write those passed in $tagformats; if false will ignore any tags not mentioned in $tagformats + /** + * Absolute filename of file to write tags to. + * + * @var string + */ + public $filename; - public $id3v2_tag_language = 'eng'; // ISO-639-2 3-character language code needed for some ID3v2 frames (http://www.id3.org/iso639-2.html) - public $id3v2_paddedlength = 4096; // minimum length of ID3v2 tags (will be padded to this length if tag data is shorter) + /** + * Array of tag formats to write ('id3v1', 'id3v2.2', 'id2v2.3', 'id3v2.4', 'ape', 'vorbiscomment', + * 'metaflac', 'real'). + * + * @var array + */ + public $tagformats = array(); - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + /** + * 2-dimensional array of tag data (ex: $data['ARTIST'][0] = 'Elvis'). + * + * @var array + */ + public $tag_data = array(array()); - // private - private $ThisFileInfo; // analysis of file before writing + /** + * Text encoding used for tag data ('ISO-8859-1', 'UTF-8', 'UTF-16', 'UTF-16LE', 'UTF-16BE', ). + * + * @var string + */ + public $tag_encoding = 'ISO-8859-1'; + + /** + * If true will erase existing tag data and write only passed data; if false will merge passed data + * with existing tag data. + * + * @var bool + */ + public $overwrite_tags = true; + + /** + * If true will erase remove all existing tags and only write those passed in $tagformats; + * If false will ignore any tags not mentioned in $tagformats. + * + * @var bool + */ + public $remove_other_tags = false; + + /** + * ISO-639-2 3-character language code needed for some ID3v2 frames. + * + * @link http://www.id3.org/iso639-2.html + * + * @var string + */ + public $id3v2_tag_language = 'eng'; + + /** + * Minimum length of ID3v2 tags (will be padded to this length if tag data is shorter). + * + * @var int + */ + public $id3v2_paddedlength = 4096; + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); + + /** + * Analysis of file before writing. + * + * @var array + */ + private $ThisFileInfo; public function __construct() { - return true; } - + /** + * @return bool + */ public function WriteTags() { if (empty($this->filename)) { @@ -83,8 +148,23 @@ class getid3_writetags $this->errors[] = 'tagformats must be an array in getid3_writetags'; return false; } + // prevent duplicate tag formats + $this->tagformats = array_unique($this->tagformats); + + // prevent trying to specify more than one version of ID3v2 tag to write simultaneously + $id3typecounter = 0; + foreach ($this->tagformats as $tagformat) { + if (substr(strtolower($tagformat), 0, 6) == 'id3v2.') { + $id3typecounter++; + } + } + if ($id3typecounter > 1) { + $this->errors[] = 'tagformats must not contain more than one version of ID3v2'; + return false; + } $TagFormatsToRemove = array(); + $AllowedTagFormats = array(); if (filesize($this->filename) == 0) { // empty file special case - allow any tag format, don't check existing format @@ -125,14 +205,12 @@ class getid3_writetags //$AllowedTagFormats = array('metaflac'); $this->errors[] = 'metaflac is not (yet) compatible with OggFLAC files'; return false; - break; case 'vorbis': $AllowedTagFormats = array('vorbiscomment'); break; default: $this->errors[] = 'metaflac is not (yet) compatible with Ogg files other than OggVorbis'; return false; - break; } break; @@ -202,7 +280,6 @@ class getid3_writetags default: $this->errors[] = 'unknown tag format "'.$tagformat.'" in $tagformats in WriteTags()'; return false; - break; } } @@ -231,9 +308,9 @@ class getid3_writetags } } - // Convert "TRACK" to "TRACKNUMBER" (if needed) for compatability with all formats - if (isset($this->tag_data['TRACK']) && !isset($this->tag_data['TRACKNUMBER'])) { - $this->tag_data['TRACKNUMBER'] = $this->tag_data['TRACK']; + // Convert "TRACK" to "TRACK_NUMBER" (if needed) for compatability with all formats + if (isset($this->tag_data['TRACK']) && !isset($this->tag_data['TRACK_NUMBER'])) { + $this->tag_data['TRACK_NUMBER'] = $this->tag_data['TRACK']; unset($this->tag_data['TRACK']); } @@ -248,7 +325,7 @@ class getid3_writetags switch ($tagformat) { case 'ape': $ape_writer = new getid3_write_apetag; - if (($ape_writer->tag_data = $this->FormatDataForAPE()) !== false) { + if ($ape_writer->tag_data = $this->FormatDataForAPE()) { $ape_writer->filename = $this->filename; if (($success = $ape_writer->WriteAPEtag()) === false) { $this->errors[] = 'WriteAPEtag() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $ape_writer->errors)))).'
    '; @@ -260,7 +337,7 @@ class getid3_writetags case 'id3v1': $id3v1_writer = new getid3_write_id3v1; - if (($id3v1_writer->tag_data = $this->FormatDataForID3v1()) !== false) { + if ($id3v1_writer->tag_data = $this->FormatDataForID3v1()) { $id3v1_writer->filename = $this->filename; if (($success = $id3v1_writer->WriteID3v1()) === false) { $this->errors[] = 'WriteID3v1() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $id3v1_writer->errors)))).'
    '; @@ -276,7 +353,10 @@ class getid3_writetags $id3v2_writer = new getid3_write_id3v2; $id3v2_writer->majorversion = intval(substr($tagformat, -1)); $id3v2_writer->paddedlength = $this->id3v2_paddedlength; - if (($id3v2_writer->tag_data = $this->FormatDataForID3v2($id3v2_writer->majorversion)) !== false) { + $id3v2_writer_tag_data = $this->FormatDataForID3v2($id3v2_writer->majorversion); + if ($id3v2_writer_tag_data !== false) { + $id3v2_writer->tag_data = $id3v2_writer_tag_data; + unset($id3v2_writer_tag_data); $id3v2_writer->filename = $this->filename; if (($success = $id3v2_writer->WriteID3v2()) === false) { $this->errors[] = 'WriteID3v2() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $id3v2_writer->errors)))).'
    '; @@ -288,7 +368,7 @@ class getid3_writetags case 'vorbiscomment': $vorbiscomment_writer = new getid3_write_vorbiscomment; - if (($vorbiscomment_writer->tag_data = $this->FormatDataForVorbisComment()) !== false) { + if ($vorbiscomment_writer->tag_data = $this->FormatDataForVorbisComment()) { $vorbiscomment_writer->filename = $this->filename; if (($success = $vorbiscomment_writer->WriteVorbisComment()) === false) { $this->errors[] = 'WriteVorbisComment() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $vorbiscomment_writer->errors)))).'
    '; @@ -300,7 +380,7 @@ class getid3_writetags case 'metaflac': $metaflac_writer = new getid3_write_metaflac; - if (($metaflac_writer->tag_data = $this->FormatDataForMetaFLAC()) !== false) { + if ($metaflac_writer->tag_data = $this->FormatDataForMetaFLAC()) { $metaflac_writer->filename = $this->filename; if (($success = $metaflac_writer->WriteMetaFLAC()) === false) { $this->errors[] = 'WriteMetaFLAC() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $metaflac_writer->errors)))).'
    '; @@ -312,7 +392,7 @@ class getid3_writetags case 'real': $real_writer = new getid3_write_real; - if (($real_writer->tag_data = $this->FormatDataForReal()) !== false) { + if ($real_writer->tag_data = $this->FormatDataForReal()) { $real_writer->filename = $this->filename; if (($success = $real_writer->WriteReal()) === false) { $this->errors[] = 'WriteReal() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $real_writer->errors)))).'
    '; @@ -325,7 +405,6 @@ class getid3_writetags default: $this->errors[] = 'Invalid tag format to write: "'.$tagformat.'"'; return false; - break; } if (!$success) { return false; @@ -335,7 +414,11 @@ class getid3_writetags } - + /** + * @param string[] $TagFormatsToDelete + * + * @return bool + */ public function DeleteTags($TagFormatsToDelete) { foreach ($TagFormatsToDelete as $DeleteTagFormat) { $success = false; // overridden if tag deletion is successful @@ -397,9 +480,8 @@ class getid3_writetags break; default: - $this->errors[] = 'Invalid tag format to delete: "'.$tagformat.'"'; + $this->errors[] = 'Invalid tag format to delete: "'.$DeleteTagFormat.'"'; return false; - break; } if (!$success) { return false; @@ -408,21 +490,30 @@ class getid3_writetags return true; } - + /** + * @param string $TagFormat + * @param array $tag_data + * + * @return bool + * @throws Exception + */ public function MergeExistingTagData($TagFormat, &$tag_data) { // Merge supplied data with existing data, if requested if ($this->overwrite_tags) { // do nothing - ignore previous data } else { -throw new Exception('$this->overwrite_tags=false is known to be buggy in this version of getID3. Check http://github.com/JamesHeinrich/getID3 for a newer version.'); - if (!isset($this->ThisFileInfo['tags'][$TagFormat])) { - return false; - } - $tag_data = array_merge_recursive($tag_data, $this->ThisFileInfo['tags'][$TagFormat]); + throw new Exception('$this->overwrite_tags=false is known to be buggy in this version of getID3. Check http://github.com/JamesHeinrich/getID3 for a newer version.'); +// if (!isset($this->ThisFileInfo['tags'][$TagFormat])) { +// return false; +// } +// $tag_data = array_merge_recursive($tag_data, $this->ThisFileInfo['tags'][$TagFormat]); } return true; } + /** + * @return array + */ public function FormatDataForAPE() { $ape_tag_data = array(); foreach ($this->tag_data as $tag_key => $valuearray) { @@ -449,8 +540,11 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve return $ape_tag_data; } - + /** + * @return array + */ public function FormatDataForID3v1() { + $tag_data_id3v1 = array(); $tag_data_id3v1['genreid'] = 255; if (!empty($this->tag_data['GENRE'])) { foreach ($this->tag_data['GENRE'] as $key => $value) { @@ -460,23 +554,29 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve } } } - $tag_data_id3v1['title'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE'] ) ? $this->tag_data['TITLE'] : array()))); - $tag_data_id3v1['artist'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST'] ) ? $this->tag_data['ARTIST'] : array()))); - $tag_data_id3v1['album'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ALBUM'] ) ? $this->tag_data['ALBUM'] : array()))); - $tag_data_id3v1['year'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['YEAR'] ) ? $this->tag_data['YEAR'] : array()))); - $tag_data_id3v1['comment'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT'] ) ? $this->tag_data['COMMENT'] : array()))); - $tag_data_id3v1['track'] = intval(getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TRACKNUMBER']) ? $this->tag_data['TRACKNUMBER'] : array())))); - if ($tag_data_id3v1['track'] <= 0) { - $tag_data_id3v1['track'] = ''; + $tag_data_id3v1['title'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE'] ) ? $this->tag_data['TITLE'] : array()))); + $tag_data_id3v1['artist'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST'] ) ? $this->tag_data['ARTIST'] : array()))); + $tag_data_id3v1['album'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ALBUM'] ) ? $this->tag_data['ALBUM'] : array()))); + $tag_data_id3v1['year'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['YEAR'] ) ? $this->tag_data['YEAR'] : array()))); + $tag_data_id3v1['comment'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT'] ) ? $this->tag_data['COMMENT'] : array()))); + $tag_data_id3v1['track_number'] = intval(getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TRACK_NUMBER']) ? $this->tag_data['TRACK_NUMBER'] : array())))); + if ($tag_data_id3v1['track_number'] <= 0) { + $tag_data_id3v1['track_number'] = ''; } $this->MergeExistingTagData('id3v1', $tag_data_id3v1); return $tag_data_id3v1; } + /** + * @param int $id3v2_majorversion + * + * @return array|false + */ public function FormatDataForID3v2($id3v2_majorversion) { $tag_data_id3v2 = array(); + $ID3v2_text_encoding_lookup = array(); $ID3v2_text_encoding_lookup[2] = array('ISO-8859-1'=>0, 'UTF-16'=>1); $ID3v2_text_encoding_lookup[3] = array('ISO-8859-1'=>0, 'UTF-16'=>1); $ID3v2_text_encoding_lookup[4] = array('ISO-8859-1'=>0, 'UTF-16'=>1, 'UTF-16BE'=>2, 'UTF-8'=>3); @@ -561,7 +661,7 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve // note: some software, notably Windows Media Player and iTunes are broken and treat files tagged with UTF-16BE (with BOM) as corrupt // therefore we force data to UTF-16LE and manually prepend the BOM $ID3v2_tag_data_converted = false; - if (!$ID3v2_tag_data_converted && ($this->tag_encoding == 'ISO-8859-1')) { + if (/*!$ID3v2_tag_data_converted && */($this->tag_encoding == 'ISO-8859-1')) { // great, leave data as-is for minimum compatability problems $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0; $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; @@ -570,15 +670,16 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve if (!$ID3v2_tag_data_converted && ($this->tag_encoding == 'UTF-8')) { do { // if UTF-8 string does not include any characters above chr(127) then it is identical to ISO-8859-1 + $value = (string) $value; // prevent warnings/errors if $value is a non-string (e.g. integer,float) for ($i = 0; $i < strlen($value); $i++) { - if (ord($value{$i}) > 127) { + if (ord($value[$i]) > 127) { break 2; } } $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0; $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; $ID3v2_tag_data_converted = true; - } while (false); + } while (false); // @phpstan-ignore-line } if (!$ID3v2_tag_data_converted) { $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 1; @@ -605,6 +706,9 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve return $tag_data_id3v2; } + /** + * @return array + */ public function FormatDataForVorbisComment() { $tag_data_vorbiscomment = $this->tag_data; @@ -612,21 +716,25 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve // and convert data to UTF-8 strings foreach ($tag_data_vorbiscomment as $tag_key => $valuearray) { foreach ($valuearray as $key => $value) { - str_replace("\r", "\n", $value); - if (strstr($value, "\n")) { - unset($tag_data_vorbiscomment[$tag_key][$key]); - $multilineexploded = explode("\n", $value); - foreach ($multilineexploded as $newcomment) { - if (strlen(trim($newcomment)) > 0) { - $tag_data_vorbiscomment[$tag_key][] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $newcomment); - } - } - } elseif (is_string($value) || is_numeric($value)) { - $tag_data_vorbiscomment[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value); + if (($tag_key == 'ATTACHED_PICTURE') && is_array($value)) { + continue; // handled separately in write.metaflac.php } else { - $this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to VorbisComment tag'; - unset($tag_data_vorbiscomment[$tag_key]); - break; + str_replace("\r", "\n", $value); + if (strstr($value, "\n")) { + unset($tag_data_vorbiscomment[$tag_key][$key]); + $multilineexploded = explode("\n", $value); + foreach ($multilineexploded as $newcomment) { + if (strlen(trim($newcomment)) > 0) { + $tag_data_vorbiscomment[$tag_key][] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $newcomment); + } + } + } elseif (is_string($value) || is_numeric($value)) { + $tag_data_vorbiscomment[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value); + } else { + $this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to VorbisComment tag'; + unset($tag_data_vorbiscomment[$tag_key]); + break; + } } } } @@ -634,13 +742,20 @@ throw new Exception('$this->overwrite_tags=false is known to be buggy in this ve return $tag_data_vorbiscomment; } + /** + * @return array + */ public function FormatDataForMetaFLAC() { // FLAC & OggFLAC use VorbisComments same as OggVorbis // but require metaflac to do the writing rather than vorbiscomment return $this->FormatDataForVorbisComment(); } + /** + * @return array + */ public function FormatDataForReal() { + $tag_data_real = array(); $tag_data_real['title'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE'] ) ? $this->tag_data['TITLE'] : array()))); $tag_data_real['artist'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST'] ) ? $this->tag_data['ARTIST'] : array()))); $tag_data_real['copyright'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COPYRIGHT']) ? $this->tag_data['COPYRIGHT'] : array()))); diff --git a/lib/getid3/write.real.php b/lib/getid3/write.real.php index 7f91165..492cc4b 100644 --- a/lib/getid3/write.real.php +++ b/lib/getid3/write.real.php @@ -1,11 +1,11 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.real.php // @@ -16,17 +16,50 @@ class getid3_write_real { + /** + * @var string + */ public $filename; + + /** + * @var array + */ public $tag_data = array(); - public $fread_buffer_size = 32768; // read buffer size in bytes - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - public $paddedlength = 512; // minimum length of CONT tag in bytes + + /** + * Read buffer size in bytes. + * + * @var int + */ + public $fread_buffer_size = 32768; + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); + + /** + * Minimum length of CONT tag in bytes. + * + * @var int + */ + public $paddedlength = 512; public function __construct() { - return true; } + /** + * @return bool + */ public function WriteReal() { // File MUST be writeable - CHMOD(646) at least if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { @@ -45,6 +78,7 @@ class getid3_write_real fclose($fp_source); return false; } + $oldChunkInfo = array(); foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { $oldChunkInfo[$chunkarray['name']] = $chunkarray; } @@ -127,8 +161,14 @@ class getid3_write_real return false; } + /** + * @param array $chunks + * + * @return string + */ public function GenerateRMFchunk(&$chunks) { $oldCONTexists = false; + $chunkNameKeys = array(); foreach ($chunks as $key => $chunk) { $chunkNameKeys[$chunk['name']] = $key; if ($chunk['name'] == 'CONT') { @@ -145,10 +185,17 @@ class getid3_write_real return $RMFchunk; } + /** + * @param array $chunks + * @param string $new_CONT_tag_data + * + * @return string + */ public function GeneratePROPchunk(&$chunks, &$new_CONT_tag_data) { $old_CONT_length = 0; $old_DATA_offset = 0; $old_INDX_offset = 0; + $chunkNameKeys = array(); foreach ($chunks as $key => $chunk) { $chunkNameKeys[$chunk['name']] = $key; if ($chunk['name'] == 'CONT') { @@ -182,6 +229,9 @@ class getid3_write_real return $PROPchunk; } + /** + * @return string + */ public function GenerateCONTchunk() { foreach ($this->tag_data as $key => $value) { // limit each value to 0xFFFF bytes @@ -211,6 +261,9 @@ class getid3_write_real return $CONTchunk; } + /** + * @return bool + */ public function RemoveReal() { // File MUST be writeable - CHMOD(646) at least if (getID3::is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { @@ -229,6 +282,7 @@ class getid3_write_real fclose($fp_source); return false; } + $oldChunkInfo = array(); foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { $oldChunkInfo[$chunkarray['name']] = $chunkarray; } diff --git a/lib/getid3/write.vorbiscomment.php b/lib/getid3/write.vorbiscomment.php index 3f427ef..31cc5a4 100644 --- a/lib/getid3/write.vorbiscomment.php +++ b/lib/getid3/write.vorbiscomment.php @@ -1,11 +1,12 @@ // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// also https://github.com/JamesHeinrich/getID3 // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // ///////////////////////////////////////////////////////////////// -// See readme.txt for more details // +// see readme.txt for more details // ///////////////////////////////////////////////////////////////// // // // write.vorbiscomment.php // @@ -17,16 +18,36 @@ class getid3_write_vorbiscomment { - + /** + * @var string + */ public $filename; + + /** + * @var array + */ public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here + + /** + * Any non-critical errors will be stored here. + * + * @var array + */ + public $warnings = array(); + + /** + * Any critical errors will be stored here. + * + * @var array + */ + public $errors = array(); public function __construct() { - return true; } + /** + * @return bool + */ public function WriteVorbisComment() { if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { @@ -63,14 +84,14 @@ class getid3_write_vorbiscomment // On top of that, if error messages are not always captured properly under Windows // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); + clearstatcache(true, $this->filename); $timestampbeforewriting = filemtime($this->filename); $commandline = GETID3_HELPERAPPSDIR.'vorbiscomment.exe -w --raw -c "'.$tempcommentsfilename.'" "'.$this->filename.'" 2>&1'; $VorbiscommentError = `$commandline`; if (empty($VorbiscommentError)) { - clearstatcache(); + clearstatcache(true, $this->filename); if ($timestampbeforewriting == filemtime($this->filename)) { $VorbiscommentError = 'File modification timestamp has not changed - it looks like the tags were not written'; } @@ -100,11 +121,19 @@ class getid3_write_vorbiscomment return true; } + /** + * @return bool + */ public function DeleteVorbisComment() { $this->tag_data = array(array()); return $this->WriteVorbisComment(); } + /** + * @param string $originalcommentname + * + * @return string + */ public function CleanVorbisCommentName($originalcommentname) { // A case-insensitive field name that may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded. // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to ASCII 0x61 through