diff --git a/lib/getid3/extension.cache.dbm.php b/lib/getid3/extension.cache.dbm.php index 010af103eb668691673110dd827e69e4310a615e..bf94ef1874d0fbb3c43ecb820bf4c6d033980ce8 100644 --- a/lib/getid3/extension.cache.dbm.php +++ b/lib/getid3/extension.cache.dbm.php @@ -224,6 +224,7 @@ class getID3_cached_dbm extends getID3 */ 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 diff --git a/lib/getid3/getid3.lib.php b/lib/getid3/getid3.lib.php index 916c43053c0889fb0df7c72c2a692f610419b73c..a24a0652525f14bcc9b0d3dd0901e754c3b3dd36 100644 --- a/lib/getid3/getid3.lib.php +++ b/lib/getid3/getid3.lib.php @@ -242,7 +242,7 @@ class getid3_lib /** * ANSI/IEEE Standard 754-1985, Standard for Binary Floating Point Arithmetic * - * @link http://www.psc.edu/general/software/packages/ieee/ieee.html + * @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 @@ -294,12 +294,12 @@ 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') { @@ -427,14 +427,20 @@ class getid3_lib * @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; } @@ -665,6 +671,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 { @@ -722,10 +729,12 @@ class getid3_lib 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); + // 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', LIBXML_NOENT); $return = self::SimpleXMLelement2array($XMLobject); - libxml_disable_entity_loader($loader); + @libxml_disable_entity_loader($loader); return $return; } return false; @@ -1536,12 +1545,21 @@ class getid3_lib public static function CopyTagsToComments(&$ThisFileInfo, $option_tags_html=true) { // Copy all entries from ['tags'] into common ['comments'] if (!empty($ThisFileInfo['tags'])) { - if (isset($ThisFileInfo['tags']['id3v1'])) { - // bubble ID3v1 to the end, if present to aid in detecting bad ID3v1 encodings - $ID3v1 = $ThisFileInfo['tags']['id3v1']; - unset($ThisFileInfo['tags']['id3v1']); - $ThisFileInfo['tags']['id3v1'] = $ID3v1; - unset($ID3v1); + + // 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) { @@ -1560,20 +1578,30 @@ 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; + + 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; @@ -1599,14 +1627,16 @@ 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]); + } } } @@ -1732,6 +1762,7 @@ class getid3_lib * @return float|bool */ public static function getFileSizeSyscall($path) { + $commandline = null; $filesize = false; if (GETID3_OS_ISWINDOWS) { @@ -1793,7 +1824,7 @@ class getid3_lib * * @return string */ - public static function mb_basename($path, $suffix = null) { + 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 bce97fb73344e45d0d38895bd3a8a1bbc3f93318..c4be4845a5c7ec3085aaa030370359a049786bc9 100644 --- a/lib/getid3/getid3.php +++ b/lib/getid3/getid3.php @@ -17,10 +17,6 @@ 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)); } @@ -57,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; } @@ -214,6 +210,140 @@ class getID3 */ 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 /** @@ -257,7 +387,7 @@ class getID3 */ protected $startup_warning = ''; - const VERSION = '1.9.20-202006061653'; + const VERSION = '1.9.21-202109171300'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; @@ -633,6 +763,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); @@ -1351,6 +1493,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 @@ -1485,7 +1637,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 { @@ -2091,19 +2243,25 @@ 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 + } + + $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 fseek($this->getid3->fp, $bytes, $whence); + return $result; } /** @@ -2219,6 +2377,8 @@ abstract class getid3_handler * @throws getid3_exception */ public function saveAttachment($name, $offset, $length, $image_mime=null) { + $fp_dest = null; + $dest = null; try { // do not extract at all diff --git a/lib/getid3/module.archive.gzip.php b/lib/getid3/module.archive.gzip.php index 18d49f38a364604fdfee5c450f01c5b66cf52853..d75778110205399cdc5ce40e25ae2d35bec842c6 100644 --- a/lib/getid3/module.archive.gzip.php +++ b/lib/getid3/module.archive.gzip.php @@ -27,12 +27,11 @@ 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 $option_gzip_parse_contents = false; + public $parse_contents = false; /** * @return bool @@ -59,7 +58,7 @@ class getid3_gzip extends getid3_handler $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; @@ -84,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']); @@ -200,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 = ''; diff --git a/lib/getid3/module.archive.rar.php b/lib/getid3/module.archive.rar.php index 4d40160d4b03ea81f4bd7a190f506d8ba4154488..f80219c38f47d5d5bcfd2e8a9cde30a458842045 100644 --- a/lib/getid3/module.archive.rar.php +++ b/lib/getid3/module.archive.rar.php @@ -21,9 +21,11 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce 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 $option_use_rar_extension = false; + public $use_php_rar_extension = true; /** * @return bool @@ -33,7 +35,7 @@ class getid3_rar extends getid3_handler $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(); @@ -50,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.tar.php b/lib/getid3/module.archive.tar.php index 5b4aa56695c90941db5ead5cd20e70d6b2481130..f8aed7952e13995fb1fec766c56f26d5c4450620 100644 --- a/lib/getid3/module.archive.tar.php +++ b/lib/getid3/module.archive.tar.php @@ -141,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' : '-'); diff --git a/lib/getid3/module.archive.xz.php b/lib/getid3/module.archive.xz.php index 1557bfb57ea3ad398010d637f4ad7c9aeb05d7e6..dbbc067ba7b9c33451a7944da15220871f07536a 100644 --- a/lib/getid3/module.archive.xz.php +++ b/lib/getid3/module.archive.xz.php @@ -20,7 +20,6 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce class getid3_xz extends getid3_handler { - /** * @return bool */ diff --git a/lib/getid3/module.archive.zip.php b/lib/getid3/module.archive.zip.php index 1ce3fb75a74512957ed3a37157c2d1e8134d435d..ef99c1410be7137dd8ca2ebd7d878ddc503cd97c 100644 --- a/lib/getid3/module.archive.zip.php +++ b/lib/getid3/module.archive.zip.php @@ -121,11 +121,11 @@ class getid3_zip extends getid3_handler !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'])) { + if (!empty($info['zip']['files']['ppt'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; - } elseif (!empty($ThisFileInfo['zip']['files']['xl'])) { + } elseif (!empty($info['zip']['files']['xl'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - } elseif (!empty($ThisFileInfo['zip']['files']['word'])) { + } elseif (!empty($info['zip']['files']['word'])) { $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; } } @@ -231,6 +231,7 @@ class getid3_zip extends getid3_handler * @return array|false */ public function ZIPparseLocalFileHeader() { + $LocalFileHeader = array(); $LocalFileHeader['offset'] = $this->ftell(); $ZIPlocalFileHeader = $this->fread(30); @@ -329,6 +330,7 @@ class getid3_zip extends getid3_handler * @return array|false */ public function ZIPparseCentralDirectory() { + $CentralDirectory = array(); $CentralDirectory['offset'] = $this->ftell(); $ZIPcentralDirectory = $this->fread(46); @@ -388,6 +390,7 @@ class getid3_zip extends getid3_handler * @return array|false */ public function ZIPparseEndOfCentralDirectory() { + $EndOfCentralDirectory = array(); $EndOfCentralDirectory['offset'] = $this->ftell(); $ZIPendOfCentralDirectory = $this->fread(22); diff --git a/lib/getid3/module.audio-video.asf.php b/lib/getid3/module.audio-video.asf.php index fce923c0272ec1dcb7eee895581f8b1d0fbdf55a..85edb7d755e68524165dfae4d50fefd31dfed576 100644 --- a/lib/getid3/module.audio-video.asf.php +++ b/lib/getid3/module.audio-video.asf.php @@ -93,6 +93,7 @@ class getid3_asf extends getid3_handler $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); @@ -283,7 +284,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; } @@ -535,7 +536,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)); @@ -1193,7 +1194,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; } diff --git a/lib/getid3/module.audio-video.flv.php b/lib/getid3/module.audio-video.flv.php index 7e684072e35f89bd36674a08ab4075d3972af52a..3b59e596e331c142c91bd281046f05d450ca72cf 100644 --- a/lib/getid3/module.audio-video.flv.php +++ b/lib/getid3/module.audio-video.flv.php @@ -161,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 diff --git a/lib/getid3/module.audio-video.ivf.php b/lib/getid3/module.audio-video.ivf.php index 791b88bc9fd947daf834fe021415bbb0f93dd3d1..293cc99b6ed6461e646309c8400ec88fbe9d3f56 100644 --- a/lib/getid3/module.audio-video.ivf.php +++ b/lib/getid3/module.audio-video.ivf.php @@ -56,6 +56,7 @@ class getid3_ivf extends getid3_handler $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) diff --git a/lib/getid3/module.audio-video.matroska.php b/lib/getid3/module.audio-video.matroska.php index a2851085907ca7d0c2d6419427b8d0ac77f43d95..128e614e2c5cceddd9ce591b2b359d1c964b5a93 100644 --- a/lib/getid3/module.audio-video.matroska.php +++ b/lib/getid3/module.audio-video.matroska.php @@ -224,14 +224,14 @@ class getid3_matroska extends getid3_handler * * @var bool */ - public static $hide_clusters = true; + public $hide_clusters = true; /** * True to parse the whole file, not only header [default: FALSE]. * * @var bool */ - public static $parse_whole_file = false; + public $parse_whole_file = false; /* * Private parser settings/placeholders. @@ -586,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']) { @@ -618,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; @@ -905,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; } @@ -1246,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'])) { diff --git a/lib/getid3/module.audio-video.mpeg.php b/lib/getid3/module.audio-video.mpeg.php index 5ca4120b1602665845eaa633d410ac3afc26d5df..0504da197c3680da3993090b59b7ee10dd7d62dd 100644 --- a/lib/getid3/module.audio-video.mpeg.php +++ b/lib/getid3/module.audio-video.mpeg.php @@ -546,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); diff --git a/lib/getid3/module.audio-video.quicktime.php b/lib/getid3/module.audio-video.quicktime.php index c176e5a03ce01ab4282f7e4cc2f8f48c7162e15a..2250e1c7934bc771269cfdf1251492541915a471 100644 --- a/lib/getid3/module.audio-video.quicktime.php +++ b/lib/getid3/module.audio-video.quicktime.php @@ -24,7 +24,18 @@ getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE_ 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; /** @@ -169,7 +180,7 @@ class getid3_quicktime extends getid3_handler } } - 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'])) { @@ -559,15 +570,28 @@ class getid3_quicktime extends getid3_handler 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'; + 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); } - $info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_structure['data'], 'description'=>'cover'); } break; @@ -727,11 +751,13 @@ class getid3_quicktime extends getid3_handler $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'; + $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 { @@ -907,13 +933,13 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in $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_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']); 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']['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']; @@ -1597,33 +1623,61 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in break; case 'NCDT': - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html + // 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 - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html + 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'] = (($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']); + $atom_structure['description'] = isset($descriptions[$atomname]) ? $descriptions[$atomname] : '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); + 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]) >= 0x00 && 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 '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 + 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 - // http://www.geocities.com/xhelmboyx/quicktime/formats/qti-layout.txt + // 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)); @@ -1720,6 +1774,7 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in '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) { @@ -2038,7 +2093,7 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in * @return array|false */ public function QuicktimeParseContainerAtom($atom_data, $baseoffset, &$atomHierarchy, $ParseAllPossibleAtoms) { - $atom_structure = false; + $atom_structure = array(); $subatomoffset = 0; $subatomcounter = 0; if ((strlen($atom_data) == 4) && (getid3_lib::BigEndian2Int($atom_data) == 0x00000000)) { @@ -2056,17 +2111,22 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in $subatomoffset += 4; continue; } - return $atom_structure; + break; } 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? - return $atom_structure; + break; } $atom_structure[$subatomcounter++] = $this->QuicktimeParseAtom($subatomname, $subatomsize, $subatomdata, $baseoffset + $subatomoffset, $atomHierarchy, $ParseAllPossibleAtoms); $subatomoffset += $subatomsize; } + + if (empty($atom_structure)) { + return false; + } + return $atom_structure; } @@ -2551,8 +2611,9 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in 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'); } @@ -2605,189 +2666,6 @@ $this->warning('incomplete/incorrect handling of "stsd" with Parrot metadata in return (isset($QuicktimeStoreFrontCodeLookup[$sfid]) ? $QuicktimeStoreFrontCodeLookup[$sfid] : 'invalid'); } - /** - * @param string $atom_data - * - * @return array - */ - 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; - $data = null; - $datalength = strlen($atom_data); - $parsed = array(); - while ($offset < $datalength) { - $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.'<br>'; - 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 diff --git a/lib/getid3/module.audio-video.riff.php b/lib/getid3/module.audio-video.riff.php index cdf553386bc34c96dce88fe49d42fdd39b3808a0..6ef1116ad13ea49279e873dbc7e36a379597449c 100644 --- a/lib/getid3/module.audio-video.riff.php +++ b/lib/getid3/module.audio-video.riff.php @@ -56,6 +56,7 @@ class getid3_riff extends getid3_handler $thisfile_riff_video = &$thisfile_riff['video']; $thisfile_riff_WAVE = array(); + $Original = array(); $Original['avdataoffset'] = $info['avdataoffset']; $Original['avdataend'] = $info['avdataend']; @@ -296,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)); @@ -307,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']); @@ -451,7 +462,62 @@ 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']; @@ -733,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']; @@ -1069,7 +1136,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, null, $this->getid3->fp); + $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()) { @@ -1172,7 +1239,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, null, $this->getid3->fp); + $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'])) { @@ -1258,7 +1325,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, null, $this->getid3->fp); + $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()) { @@ -1514,6 +1581,9 @@ class getid3_riff extends getid3_handler $RIFFchunk = false; $FoundAllChunksWeNeed = false; + $LISTchunkParent = null; + $LISTchunkMaxOffset = null; + $AC3syncwordBytes = pack('n', getid3_ac3::syncword); // 0x0B77 -> "\x0B\x77" try { $this->fseek($startoffset); @@ -1557,7 +1627,7 @@ class getid3_riff extends getid3_handler // MP3 if (getid3_mp3::MPEGaudioHeaderBytesValid($FirstFourBytes)) { $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename, null, $this->getid3->fp); + $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__); @@ -1575,11 +1645,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, null, $this->getid3->fp); + $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); @@ -1640,7 +1709,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, null, $this->getid3->fp); + $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__); @@ -1652,12 +1721,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, null, $this->getid3->fp); + $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']; } @@ -1673,6 +1742,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); } @@ -1691,7 +1762,7 @@ class getid3_riff extends getid3_handler // This is probably DTS data $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename, null, $this->getid3->fp); + $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(); @@ -1732,6 +1803,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 @@ -2076,6 +2149,7 @@ class getid3_riff extends getid3_handler */ 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 diff --git a/lib/getid3/module.audio-video.swf.php b/lib/getid3/module.audio-video.swf.php index fc4a5c526589e693590dc153275db8f8ce84538f..a9b2c1a657e1a6091e2548e08b708c57bfcf9d2f 100644 --- a/lib/getid3/module.audio-video.swf.php +++ b/lib/getid3/module.audio-video.swf.php @@ -20,6 +20,11 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce 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; /** @@ -115,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.dsdiff.php b/lib/getid3/module.audio.dsdiff.php index bed2afca5976e8ebc872e20aaf0243da9741b662..20fb0d9a8abd6667157ced9b79260c4a2e685ea7 100644 --- a/lib/getid3/module.audio.dsdiff.php +++ b/lib/getid3/module.audio.dsdiff.php @@ -46,9 +46,10 @@ class getid3_dsdiff extends getid3_handler $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 '.$thisChunk['offset'].', found insufficient data in file, aborting parsing'); + $this->error('Expecting chunk header at offset '.(isset($thisChunk['offset']) ? $thisChunk['offset'] : 'N/A').', found insufficient data in file, aborting parsing'); break; } $thisChunk = array(); @@ -209,7 +210,7 @@ class getid3_dsdiff extends getid3_handler getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename, null, $this->getid3->fp); + $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()) { @@ -292,7 +293,7 @@ class getid3_dsdiff extends getid3_handler } /** - * @param int $cmtType + * @param int $markType * * @return string */ diff --git a/lib/getid3/module.audio.flac.php b/lib/getid3/module.audio.flac.php index 1cea4364c2a0e8d7a4195466ac793a8be875caf8..014061da943c2b337830b5d55d6140ffa6a06cfe 100644 --- a/lib/getid3/module.audio.flac.php +++ b/lib/getid3/module.audio.flac.php @@ -402,6 +402,7 @@ class getid3_flac extends getid3_handler 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))); diff --git a/lib/getid3/module.audio.lpac.php b/lib/getid3/module.audio.lpac.php index f49e2440992047136a48ddd6ad72be6b3324ae34..6656a1410dd7e99f52d6e58339cbd916dbaef23b 100644 --- a/lib/getid3/module.audio.lpac.php +++ b/lib/getid3/module.audio.lpac.php @@ -34,6 +34,7 @@ class getid3_lpac extends getid3_handler $this->error('Expected "LPAC" at offset '.$info['avdataoffset'].', found "'.$StreamMarker.'"'); return false; } + $flags = array(); $info['avdataoffset'] += 14; $info['fileformat'] = 'lpac'; diff --git a/lib/getid3/module.audio.midi.php b/lib/getid3/module.audio.midi.php index eaef9cbaf9c839549245c041b56cf3e0c82539bc..d5fe81a2bd27acd89d4211f040c4b7be80b41662 100644 --- a/lib/getid3/module.audio.midi.php +++ b/lib/getid3/module.audio.midi.php @@ -24,6 +24,8 @@ 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; @@ -61,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())) { @@ -85,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']); diff --git a/lib/getid3/module.audio.mp3.php b/lib/getid3/module.audio.mp3.php index 26b28068f8b50f2c35efde8d8d0832745e3c1b70..1f5a566b831cf514194ff4cc4fd4c30b702f989a 100644 --- a/lib/getid3/module.audio.mp3.php +++ b/lib/getid3/module.audio.mp3.php @@ -18,12 +18,6 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce exit; } -// 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); - class getid3_mp3 extends getid3_handler { @@ -35,6 +29,15 @@ class getid3_mp3 extends getid3_handler */ public $allow_bruteforce = false; + /** + * 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 */ @@ -55,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 '; @@ -121,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; } } @@ -295,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']); } @@ -488,7 +498,7 @@ class getid3_mp3 extends getid3_handler 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; } @@ -722,197 +732,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); - $thisfile_mpeg_audio_lame['numeric_version'] = str_replace('LAME', '', $thisfile_mpeg_audio_lame['short_version']); - if (preg_match('#^LAME([0-9\\.a-z]+)#', $thisfile_mpeg_audio_lame['long_version'], $matches)) { + + //$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]; } - 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 - - // 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']); + 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 + + // 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_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)); + $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)); - if ($thisfile_mpeg_audio_lame_raw['RGAD_track'] != 0) { + if ($thisfile_mpeg_audio_lame_raw['RGAD_track'] != 0) { - $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']); + $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']); - if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { - $info['replay_gain']['track']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; + 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']); } - $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_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']; + } + $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']); } - $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']); - } - // 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; - - // 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']; - } + // 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; + + // 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 $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 $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 $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)); + // 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) { + // LAME CBR + if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { - $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']; - //} + $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']; + //} - } + } + } } } @@ -1009,6 +1022,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']); + } } @@ -1130,8 +1159,9 @@ class getid3_mp3 extends getid3_handler $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; @@ -1139,6 +1169,7 @@ class getid3_mp3 extends getid3_handler $nextframetestarray = array('error' => array(), 'warning' => array(), 'avdataend' => $info['avdataend'], 'avdataoffset'=>$info['avdataoffset']); if ($this->decodeMPEGaudioHeader($nextframetestoffset, $nextframetestarray, false)) { + 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'])) { @@ -1273,6 +1304,7 @@ class getid3_mp3 extends getid3_handler $LongMPEGbitrateLookup = array(); $LongMPEGpaddingLookup = array(); $LongMPEGfrequencyLookup = array(); + $Distribution = array(); $Distribution['bitrate'] = array(); $Distribution['frequency'] = array(); $Distribution['layer'] = array(); @@ -1433,6 +1465,9 @@ 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)) { @@ -1471,7 +1506,24 @@ 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; @@ -1511,9 +1563,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.')'); } } } @@ -1558,6 +1610,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; } @@ -1887,6 +1940,7 @@ 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 diff --git a/lib/getid3/module.audio.mpc.php b/lib/getid3/module.audio.mpc.php index 4f6f5c89b2272cdd96bea4c2ea988e318cb1fa6f..d7f2d99f9afe57735a581cc994e4fd4111bb5f16 100644 --- a/lib/getid3/module.audio.mpc.php +++ b/lib/getid3/module.audio.mpc.php @@ -348,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)); diff --git a/lib/getid3/module.audio.ogg.php b/lib/getid3/module.audio.ogg.php index fe092d9d94d190bafb504bfa610296abe0699adb..5786fd0d5970fc06d3f33c642ea264b29b9c4f38 100644 --- a/lib/getid3/module.audio.ogg.php +++ b/lib/getid3/module.audio.ogg.php @@ -529,6 +529,7 @@ class getid3_ogg extends getid3_handler */ 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()); @@ -680,35 +681,39 @@ class getid3_ogg extends getid3_handler $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()); + 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); - //$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()); + //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset']; + } else { + $this->warning('failed to ParseOggPageHeader() 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']); diff --git a/lib/getid3/module.audio.optimfrog.php b/lib/getid3/module.audio.optimfrog.php index 7e888ccdd4c138f880e94d1be714fb2bb359c7b1..d84c2c48e0124637bc7c96bf8b077f01b3a8540b 100644 --- a/lib/getid3/module.audio.optimfrog.php +++ b/lib/getid3/module.audio.optimfrog.php @@ -188,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; diff --git a/lib/getid3/module.audio.shorten.php b/lib/getid3/module.audio.shorten.php index 533208faf23a2e70ab98b6c86d3f8c730a2b710d..673a485856af4ebd2fce56034f2a6e9dc2a3b38f 100644 --- a/lib/getid3/module.audio.shorten.php +++ b/lib/getid3/module.audio.shorten.php @@ -17,6 +17,7 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers exit; } + class getid3_shorten extends getid3_handler { /** diff --git a/lib/getid3/module.audio.tak.php b/lib/getid3/module.audio.tak.php index 5d4ada56762876920118e6c961289683977bf04b..0391d05bc12b8701967ae5cc9acee2bae22fb4e0 100644 --- a/lib/getid3/module.audio.tak.php +++ b/lib/getid3/module.audio.tak.php @@ -20,7 +20,9 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce class getid3_tak extends getid3_handler { - + /** + * @return bool + */ public function Analyze() { $info = &$this->getid3->info; diff --git a/lib/getid3/module.audio.wavpack.php b/lib/getid3/module.audio.wavpack.php index acb0d3eed0f7d7f8c111932cf546ff63d16871d8..dfeb85d435bae9cfeaee93b992ff198b6ab9e76e 100644 --- a/lib/getid3/module.audio.wavpack.php +++ b/lib/getid3/module.audio.wavpack.php @@ -19,6 +19,14 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce } 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 */ @@ -27,6 +35,7 @@ class getid3_wavpack extends getid3_handler $this->fseek($info['avdataoffset']); + $found_blocks = array(); while (true) { $wavpackheader = $this->fread(32); @@ -158,6 +167,7 @@ class getid3_wavpack extends getid3_handler $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 @@ -237,6 +247,10 @@ class getid3_wavpack extends getid3_handler // 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; @@ -317,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; @@ -369,7 +387,6 @@ class getid3_wavpack extends getid3_handler $info['audio']['dataformat'] = 'wvc'; } - return true; } diff --git a/lib/getid3/module.graphic.bmp.php b/lib/getid3/module.graphic.bmp.php index 276688d4d8cd012376c11f2a0b880e62e5a8918a..8456570112f00b7925eef3946e2d1b21e0c882cf 100644 --- a/lib/getid3/module.graphic.bmp.php +++ b/lib/getid3/module.graphic.bmp.php @@ -20,7 +20,18 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce class getid3_bmp extends getid3_handler { + /** + * return BMP palette + * + * @var bool + */ public $ExtractPalette = false; + + /** + * return image data + * + * @var bool + */ public $ExtractData = false; /** @@ -498,6 +509,7 @@ class getid3_bmp extends getid3_handler 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)); @@ -533,7 +545,6 @@ class getid3_bmp extends getid3_handler // 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); $paletteindexes = array(); for ($i = 0; $i < ceil($secondbyte / 2); $i++) { $paletteindexbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); diff --git a/lib/getid3/module.graphic.jpg.php b/lib/getid3/module.graphic.jpg.php index 5079119d29ba54873299b8c8335800dfc5a2d448..c6ffad7e7087a88a6f2d08f9bf55dfb0377cf7e0 100644 --- a/lib/getid3/module.graphic.jpg.php +++ b/lib/getid3/module.graphic.jpg.php @@ -62,7 +62,7 @@ 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; @@ -71,17 +71,14 @@ class getid3_jpg extends getid3_handler 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) { + 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).'")'); diff --git a/lib/getid3/module.graphic.pcd.php b/lib/getid3/module.graphic.pcd.php index 85fb4c96ca3ac78807d4f59854e6e5f3b1cd322f..6f2aaac60731c5c98dfddac58fb58625e9d54ae1 100644 --- a/lib/getid3/module.graphic.pcd.php +++ b/lib/getid3/module.graphic.pcd.php @@ -54,6 +54,7 @@ class getid3_pcd extends getid3_handler } 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 diff --git a/lib/getid3/module.graphic.png.php b/lib/getid3/module.graphic.png.php index 018062c3811922b733d2edb99c8bc6c680ec4e8a..e29eeb79e647d2ae5df36775d28331d60ae0b29c 100644 --- a/lib/getid3/module.graphic.png.php +++ b/lib/getid3/module.graphic.png.php @@ -57,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); @@ -66,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; diff --git a/lib/getid3/module.graphic.tiff.php b/lib/getid3/module.graphic.tiff.php index 5d4049474bb90ec23868e43e02f4f810d41669fa..e4f3cf7bdbc6aeb23e9840ab06dffc45d06223a3 100644 --- a/lib/getid3/module.graphic.tiff.php +++ b/lib/getid3/module.graphic.tiff.php @@ -125,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 diff --git a/lib/getid3/module.misc.cue.php b/lib/getid3/module.misc.cue.php index a66e2eb2548ff9aaebc92faf4d6f049117aeac9c..5082f2fb59f5b9b1d1e37fc39e4df7c4b95e196c 100644 --- a/lib/getid3/module.misc.cue.php +++ b/lib/getid3/module.misc.cue.php @@ -94,10 +94,10 @@ class getid3_cue extends getid3_handler { //-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': @@ -106,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; @@ -132,7 +132,7 @@ 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; } } diff --git a/lib/getid3/module.misc.iso.php b/lib/getid3/module.misc.iso.php index efca73aa13b115810e2d431ac8d35cf2344982a0..939bd631e1e55d825260eee4bb74da03eb404608 100644 --- a/lib/getid3/module.misc.iso.php +++ b/lib/getid3/module.misc.iso.php @@ -53,8 +53,10 @@ 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; @@ -250,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(); diff --git a/lib/getid3/module.misc.pdf.php b/lib/getid3/module.misc.pdf.php index 1aca6ae35225628febbc025e6de79e5aaa1c7719..975fe3dfb4d8c40940e008cfbff143207a485dbc 100644 --- a/lib/getid3/module.misc.pdf.php +++ b/lib/getid3/module.misc.pdf.php @@ -20,14 +20,18 @@ if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that acce class getid3_pdf extends getid3_handler { - public $returnXREF = false; // return full details of PDF Cross-Reference Table (XREF) + /** 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]); @@ -62,39 +66,45 @@ class getid3_pdf extends getid3_handler } } } + + 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; } - $this->fseek($offset); - $line = rtrim($this->fgets()); - if (preg_match('#^'.$objectNumber.' ([0-9]+) obj#', $line, $matches)) { - if (strlen($line) > strlen($matches[0])) { - // object header line not actually on its own line, rewind file pointer to start reading data - $this->fseek($offset + strlen($matches[0])); - } - $objectData = ''; - while (true) { - $line = $this->fgets(); - if (rtrim($line) == 'endobj') { - break; - } - $objectData .= $line; - } - 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 + if (($maxObjLengths[$objectNumber] > 0) && ($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; } - } else { - $this->error('Unexpected structure "'.$line.'" at offset '.$offset); - break; } } if (!$this->returnXREF) { - unset($info['pdf']['xref']['offset'], $info['pdf']['xref']['generation'], $info['pdf']['xref']['entry']); + unset($info['pdf']['xref']['offset'], $info['pdf']['xref']['generation'], $info['pdf']['xref']['entry'], $info['pdf']['xref']['xref_offsets']); } } else { @@ -123,6 +133,7 @@ class getid3_pdf extends getid3_handler $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++) { diff --git a/lib/getid3/module.misc.torrent.php b/lib/getid3/module.misc.torrent.php new file mode 100644 index 0000000000000000000000000000000000000000..1e25f95f0fa09673f2d1751c6751c4f73b14222c --- /dev/null +++ b/lib/getid3/module.misc.torrent.php @@ -0,0 +1,247 @@ +<?php + +///////////////////////////////////////////////////////////////// +/// getID3() by James Heinrich <info@getid3.org> // +// 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 i<integer>e: + // i90e + $value = $this->ReadSequentialDigits($TORRENT, $offset, true); + if ($TORRENT[$offset++] == 'e') { +//echo '<li>int: '.$value.'</li>'; + 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.'<ul>'; + $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 '</ul>'; + return $values; + } + $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1)); + return false; + + } elseif ($type == 'l') { + +//echo 'LIST @ '.$offset.'<ul>'; + // 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 '</ul>'; + return $values; + } + $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1)); + return false; + + } elseif (ctype_digit($type)) { + + // Strings are stored as <length of string>:<string>: + // 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 '<li>string: '.$string.'</li>'; + 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 26be982c73a25c1ad31b51fa893b9b0e51eb0de5..c5502133fcbb31f2aa435165a2ab231443f1413e 100644 --- a/lib/getid3/module.tag.apetag.php +++ b/lib/getid3/module.tag.apetag.php @@ -360,6 +360,7 @@ class getid3_apetag extends getid3_handler // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html // shortcut + $headerfooterinfo = array(); $headerfooterinfo['raw'] = array(); $headerfooterinfo_raw = &$headerfooterinfo['raw']; @@ -389,6 +390,7 @@ class getid3_apetag extends getid3_handler // "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); diff --git a/lib/getid3/module.tag.id3v1.php b/lib/getid3/module.tag.id3v1.php index 16dcf253b655d39be217499ccdf7c22e8c9c56e5..b1de25784bb92982fb3d80bcd05afe87978dade0 100644 --- a/lib/getid3/module.tag.id3v1.php +++ b/lib/getid3/module.tag.id3v1.php @@ -31,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)); @@ -297,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', diff --git a/lib/getid3/module.tag.id3v2.php b/lib/getid3/module.tag.id3v2.php index 85dd7a147a1482c7957691ae2d050eb1df2d7d9f..99e56a5fdb066b33a615d93945a0629c643876ab 100644 --- a/lib/getid3/module.tag.id3v2.php +++ b/lib/getid3/module.tag.id3v2.php @@ -345,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); @@ -978,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. // <Header for 'Unsynchronised lyrics/text transcription', ID: 'USLT'> @@ -994,24 +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 - } - $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); + 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); - 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']); @@ -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') { @@ -1956,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']); @@ -2444,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 @@ -2645,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 diff --git a/lib/getid3/module.tag.lyrics3.php b/lib/getid3/module.tag.lyrics3.php index b2375057e79106a00c552ac2f0e6c2c76440cf20..c8b2cf6305984d68dd29ae291d521507dd037134 100644 --- a/lib/getid3/module.tag.lyrics3.php +++ b/lib/getid3/module.tag.lyrics3.php @@ -33,6 +33,9 @@ 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 = (int) substr($lyrics3_id3v1, 0, 6); // Lyrics3size $lyrics3end = substr($lyrics3_id3v1, 6, 9); // LYRICSEND or LYRICS200 diff --git a/lib/getid3/module.tag.nikon-nctg.php b/lib/getid3/module.tag.nikon-nctg.php new file mode 100644 index 0000000000000000000000000000000000000000..46e2f21d895551d5bf10f8d612cb2d9b01efe2d2 --- /dev/null +++ b/lib/getid3/module.tag.nikon-nctg.php @@ -0,0 +1,1448 @@ +<?php + +///////////////////////////////////////////////////////////////// +/// getID3() by James Heinrich <info@getid3.org> // +// 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 <starosekpd@gmail.com> + * @author Phil Harvey <philharvey66@gmail.com> + * + * @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<string, mixed> + */ + 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 db29601a3c68f5fcd7b6771436688d0d765bdbad..23c0d1abc1b7232cf44bafd921b19374ca95660e 100644 --- a/lib/getid3/module.tag.xmp.php +++ b/lib/getid3/module.tag.xmp.php @@ -81,7 +81,7 @@ 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|boolean $headerdata - Array of JPEG header segments, + * @return array|false $headerdata - Array of JPEG header segments, * FALSE - if headers could not be read */ public function _get_jpeg_header_data($filename) @@ -192,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|boolean $xmp_data - the string of raw XML text, - * 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) { @@ -201,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); - - 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 https://www.getid3.org/phpBB3/viewtopic.php?f=4&t=1153) + 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); + + // 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; } @@ -225,7 +228,7 @@ class Image_XMP * which contains all the XMP (XML) information. * * @param string $xmltext - a string containing the XMP data (XML) to be parsed - * @return array|boolean $xmp_array - an array containing all xmp details retrieved, + * @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) diff --git a/lib/getid3/write.id3v2.php b/lib/getid3/write.id3v2.php index ae2313eda11db94421ea8e91dcbe7674999a20a4..7385a459815d8fab0c6be18fe7c865b242514189 100644 --- a/lib/getid3/write.id3v2.php +++ b/lib/getid3/write.id3v2.php @@ -1984,8 +1984,8 @@ class getid3_write_id3v2 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; } } diff --git a/lib/getid3/write.php b/lib/getid3/write.php index 73eea6db84f150c954b15afd12f7d35b2c15259f..1167e0f76df3312c59f1e24da4769f98b78d0977 100644 --- a/lib/getid3/write.php +++ b/lib/getid3/write.php @@ -576,6 +576,7 @@ class getid3_writetags 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); @@ -669,6 +670,7 @@ class getid3_writetags 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) { break 2; @@ -753,6 +755,7 @@ class getid3_writetags * @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 38c0648c738b448c8cf911e26a55d0dd94d43d5b..492cc4bb56823e9cef8da7ee65862611990937d0 100644 --- a/lib/getid3/write.real.php +++ b/lib/getid3/write.real.php @@ -282,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; }