diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ab257455..dbeea195 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -20,6 +20,7 @@ jobs: - "7.4" - "8.0" - "8.1" + - "8.2" steps: - uses: "actions/checkout@v2" - uses: "shivammathur/setup-php@v2" @@ -27,7 +28,7 @@ jobs: php-version: "${{ matrix.php-version }}" ini-values: error_reporting=-1, display_errors=On coverage: "none" - - uses: "ramsey/composer-install@v1" + - uses: "ramsey/composer-install@v2" - name: "Run the linter" run: "composer lint -- --colors" @@ -41,7 +42,7 @@ jobs: php-version: "7.4" tools: "phpstan:0.12.99" coverage: "none" - - uses: "ramsey/composer-install@v1" + - uses: "ramsey/composer-install@v2" - name: "Run PHPStan" run: "phpstan analyse -c phpstan.neon -l 4 src tests" diff --git a/changelog.txt b/changelog.txt index 5b8ffa5a..e6a6e586 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,30 @@ Version History =============== +1.9.22: [2022-09-29] James Heinrich :: 1.9.22-202207161647 + * bugfix #387 fails to detect h265 video codec (QuickTime) + * bugfix #385 Quicktime extended atom size + * bugfix #378 AAC bitrate cache warning + * bugfix #376 simplexml_load_string improvments + * bugfix #374 MOD improved SoundTracker support + * bugfix #371 fragmented MP4 unsupported warning + * bugfix #369 fix remote URLs pattern + * bugfix #366 change @error-suppress to isset (quicktime) + * bugfix #365 ZIP array offset on value of type int + * bugfix #364 add support for ANIMEXTS1.0 in GIF files + * bugfix #363 ASF improve support of Header Extension Object data + * bugfix #362 version update for ramsey/composer-install + * bugfix #359 MPEG-2 aspect ratio divide-by-zero + * bugfix #358 free format mp3 bitrate + * bugfix #355 undefined array key in ID3v2 chapters + * bugfix #352 avoid false detection of Musepack format + * bugfix #351 Incorrect length passed to fread on a flac file + * bugfix #348 more targeted usage of clearstatcache calls + * bugfix #347 fixed reported by PHPStan v0.12.99 + * bugfix QuickTime support 'ID32' frame (ID3v2 inside QT) + * bugfix fix various PHP 8.1 issues + * bugfix PDF prevent undefined index + 1.9.21: [2021-09-22] James Heinrich :: 1.9.21-202109171300 » add support for RIFF.guan ¤ add ID3v1 genres 148-191 diff --git a/composer.json b/composer.json index 8daacdf8..a387980c 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ "php-parallel-lint/php-parallel-lint": "^1.0" }, "scripts": { - "lint": "parallel-lint --exclude vendor --exclude .git .", + "lint": "parallel-lint --show-deprecated --exclude vendor --exclude .git .", "test": [ "composer lint" ] diff --git a/readme.txt b/readme.txt index 0888bc4d..1f79d75d 100644 --- a/readme.txt +++ b/readme.txt @@ -188,8 +188,8 @@ if ($fp_remote = fopen($remotefilename, 'rb')) { $remote_headers = array_change_key_case(get_headers($remotefilename, 1), CASE_LOWER); $remote_filesize = (isset($remote_headers['content-length']) ? (is_array($remote_headers['content-length']) ? $remote_headers['content-length'][count($remote_headers['content-length']) - 1] : $remote_headers['content-length']) : null); - // Initialize getID3 engine - $getID3 = new getID3; + // Initialize GetID3 engine + $getID3 = new GetID3; $ThisFileInfo = $getID3->analyze($localtempfilename, $remote_filesize, basename($remotefilename)); diff --git a/src/GetID3.php b/src/GetID3.php index b747e7af..5b884919 100644 --- a/src/GetID3.php +++ b/src/GetID3.php @@ -119,7 +119,7 @@ class GetID3 public $option_md5_data = false; /** - * Use MD5 of source file if availble - only FLAC and OptimFROG + * Use MD5 of source file if available - only FLAC and OptimFROG * * @var bool */ @@ -319,7 +319,7 @@ class GetID3 */ protected $startup_warning = ''; - const VERSION = '2.0.x-202112151109'; + const VERSION = '2.0.x-202207161647'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; @@ -887,14 +887,15 @@ public function GetFileFormatArray() { 'mime_type' => 'audio/x-monkeys-audio', ), -// has been known to produce false matches in random files (e.g. JPEGs), leave out until more precise matching available -// // MOD - audio - MODule (assorted sub-formats) -// 'mod' => array( -// 'pattern' => '^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)', -// 'module' => 'Audio\\Mod', -// 'option' => 'mod', -// 'mime_type' => 'audio/mod', -// ), + + // MOD - audio - MODule (SoundTracker) + 'mod' => array( + //'pattern' => '^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)', // has been known to produce false matches in random files (e.g. JPEGs), leave out until more precise matching available + 'pattern' => '^.{1080}(M\\.K\\.)', + 'module' => 'Audio\\Mod', + 'option' => 'mod', + 'mime_type' => 'audio/mod', + ), // MOD - audio - MODule (Impulse Tracker) 'it' => array( diff --git a/src/Module/Archive/Zip.php b/src/Module/Archive/Zip.php index 6457e069..0d1e9166 100644 --- a/src/Module/Archive/Zip.php +++ b/src/Module/Archive/Zip.php @@ -297,7 +297,7 @@ public function ZIPparseLocalFileHeader() { $DataDescriptor = $this->fread(16); $LocalFileHeader['data_descriptor']['signature'] = Utils::LittleEndian2Int(substr($DataDescriptor, 0, 4)); if ($LocalFileHeader['data_descriptor']['signature'] != 0x08074B50) { // "PK\x07\x08" - $this->getid3->warning('invalid Local File Header Data Descriptor Signature at offset '.($this->ftell() - 16).' - expecting 08 07 4B 50, found '.Utils::PrintHexBytes($LocalFileHeader['data_descriptor']['signature'])); + $this->getid3->warning('invalid Local File Header Data Descriptor Signature at offset '.($this->ftell() - 16).' - expecting 08 07 4B 50, found '.Utils::PrintHexBytes(substr($DataDescriptor, 0, 4))); $this->fseek($LocalFileHeader['offset']); // seek back to where filepointer originally was so it can be handled properly return false; } diff --git a/src/Module/Audio/Aac.php b/src/Module/Audio/Aac.php index 40e889f5..cc7174c3 100644 --- a/src/Module/Audio/Aac.php +++ b/src/Module/Audio/Aac.php @@ -400,7 +400,7 @@ public function getAACADTSheaderFilepointer($MaxFramesToScan=1000000, $ReturnExt if (!isset($BitrateCache[$FrameLength])) { $BitrateCache[$FrameLength] = ($info['aac']['header']['sample_frequency'] / 1024) * $FrameLength * 8; } - Utils::safe_inc($info['aac']['bitrate_distribution'][$BitrateCache[$FrameLength]], 1); + Utils::safe_inc($info['aac']['bitrate_distribution'][(string)$BitrateCache[$FrameLength]], 1); $info['aac'][$framenumber]['aac_frame_length'] = $FrameLength; diff --git a/src/Module/Audio/Mod.php b/src/Module/Audio/Mod.php index e7f075a3..b7874d83 100644 --- a/src/Module/Audio/Mod.php +++ b/src/Module/Audio/Mod.php @@ -16,6 +16,7 @@ ///////////////////////////////////////////////////////////////// use JamesHeinrich\GetID3\Module\Handler; +use JamesHeinrich\GetID3\Utils; class Mod extends Handler { @@ -30,9 +31,17 @@ public function Analyze() { return $this->getITheaderFilepointer(); } elseif (preg_match('#^Extended Module#', $fileheader)) { return $this->getXMheaderFilepointer(); - } elseif (preg_match('#^.{44}SCRM#', $fileheader)) { + } elseif (preg_match('#^.{44}SCRM#s', $fileheader)) { return $this->getS3MheaderFilepointer(); - } elseif (preg_match('#^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)#', $fileheader)) { + //} elseif (preg_match('#^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)#s', $fileheader)) { + } elseif (preg_match('#^.{1080}(M\\.K\\.)#s', $fileheader)) { + /* + The four letters "M.K." - This is something Mahoney & Kaktus inserted when they + increased the number of samples from 15 to 31. If it's not there, the module/song + uses 15 samples or the text has been removed to make the module harder to rip. + Startrekker puts "FLT4" or "FLT8" there instead. + If there are more than 64 patterns, PT2.3 will insert M!K! here. + */ return $this->getMODheaderFilepointer(); } $this->error('This is not a known type of MOD file'); @@ -44,17 +53,48 @@ public function Analyze() { */ public function getMODheaderFilepointer() { $info = &$this->getid3->info; - $this->fseek($info['avdataoffset'] + 1080); - $FormatID = $this->fread(4); - if (!preg_match('#^(M.K.|[5-9]CHN|[1-3][0-9]CH)$#', $FormatID)) { - $this->error('This is not a known type of MOD file'); + $this->fseek($info['avdataoffset']); + $filedata = $this->fread(1084); + //if (!preg_match('#^(M.K.|[5-9]CHN|[1-3][0-9]CH)$#', $FormatID)) { + if (substr($filedata, 1080, 4) == 'M.K.') { + + // + 0 song/module working title + // + 20 15 sample headers (see below) + // + 470 song length (number of steps in pattern table) + // + 471 song speed in beats per minute (see below) + // + 472 pattern step table + $offset = 0; + $info['mod']['title'] = rtrim(substr($filedata, $offset, 20), "\x00"); $offset += 20; + + $info['tags']['mod']['title'] = array($info['mod']['title']); + + for ($samplenumber = 0; $samplenumber <= 30; $samplenumber++) { + $sampledata = array(); + $sampledata['name'] = substr($filedata, $offset, 22); $offset += 22; + $sampledata['length'] = Utils::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['volume'] = Utils::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['repeat_offset'] = Utils::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $sampledata['repeat_length'] = Utils::BigEndian2Int(substr($filedata, $offset, 2)); $offset += 2; + $info['mod']['samples'][$samplenumber] = $sampledata; + } + + $info['mod']['song_length'] = Utils::BigEndian2Int(substr($filedata, $offset++, 1));// Songlength. Range is 1-128. + $info['mod']['bpm'] = Utils::BigEndian2Int(substr($filedata, $offset++, 1));// This byte is set to 127, so that old trackers will search through all patterns when loading. Noisetracker uses this byte for restart, ProTracker doesn't. + + for ($songposition = 0; $songposition <= 127; $songposition++) { + // Song positions 0-127. Each hold a number from 0-63 (or 0-127) + // that tells the tracker what pattern to play at that position. + $info['mod']['song_positions'][$songposition] = Utils::BigEndian2Int(substr($filedata, $offset++, 1)); + } + + } else { + $this->error('unknown MOD ID at offset 1080: '.Utils::PrintHexBytes(substr($filedata, 1080, 4))); return false; } - $info['fileformat'] = 'mod'; - $this->error('MOD parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); - return false; +$this->warning('MOD (SoundTracker) parsing incomplete in this version of getID3() ['.$this->getid3->version().']'); + return true; } /** diff --git a/src/Module/Audio/Mp3.php b/src/Module/Audio/Mp3.php index 03a74dea..2f565917 100644 --- a/src/Module/Audio/Mp3.php +++ b/src/Module/Audio/Mp3.php @@ -315,6 +315,10 @@ public function GuessEncoderOptions() { $encoder_options .= ' -b'.$thisfile_mpeg_audio_lame['bitrate_min']; } + if (isset($thisfile_mpeg_audio['bitrate']) && $thisfile_mpeg_audio['bitrate'] === 'free') { + $encoder_options .= ' --freeformat'; + } + if (!empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev']) || !empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'])) { $encoder_options .= ' --nogap'; } @@ -750,7 +754,8 @@ public function decodeMPEGaudioHeader($offset, &$info, $recursivesearch=true, $S 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 + // https://wiki.hydrogenaud.io/index.php/LAME#VBR_header_and_LAME_tag + // https://hydrogenaud.io/index.php?topic=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 @@ -786,7 +791,7 @@ public function decodeMPEGaudioHeader($offset, &$info, $recursivesearch=true, $S $thisfile_mpeg_audio_lame['lowpass_frequency'] = Utils::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA6, 1)) * 100; // bytes $A7-$AE Replay Gain - // http://privatewww.essex.ac.uk/~djmrob/replaygain/rg_data_format.html + // https://web.archive.org/web/20021015212753/http://privatewww.essex.ac.uk/~djmrob/replaygain/rg_data_format.html // bytes $A7-$AA : 32 bit floating point "Peak signal amplitude" if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.94b') { // LAME 3.94a16 and later - 9.23 fixed point @@ -914,7 +919,7 @@ public function decodeMPEGaudioHeader($offset, &$info, $recursivesearch=true, $S // LAME CBR - if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { + if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1 && $thisfile_mpeg_audio['bitrate'] !== 'free') { $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; $thisfile_mpeg_audio['bitrate'] = self::ClosestStandardMP3Bitrate($thisfile_mpeg_audio['bitrate']); diff --git a/src/Module/Audio/Ogg.php b/src/Module/Audio/Ogg.php index 4606b089..9cfd9c4e 100644 --- a/src/Module/Audio/Ogg.php +++ b/src/Module/Audio/Ogg.php @@ -185,7 +185,7 @@ public function Analyze() { if ($info['ogg']['pageheader']['theora']['pixel_aspect_denominator'] > 0) { $info['video']['pixel_aspect_ratio'] = (float) $info['ogg']['pageheader']['theora']['pixel_aspect_numerator'] / $info['ogg']['pageheader']['theora']['pixel_aspect_denominator']; } - $this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable'); +$this->warning('Ogg Theora (v3) not fully supported in this version of getID3 ['.$this->getid3->version().'] -- bitrate, playtime and all audio data are currently unavailable'); } elseif (substr($filedata, 0, 8) == "fishead\x00") { diff --git a/src/Module/AudioVideo/Asf.php b/src/Module/AudioVideo/Asf.php index 6136fcb0..67c4ad96 100644 --- a/src/Module/AudioVideo/Asf.php +++ b/src/Module/AudioVideo/Asf.php @@ -22,6 +22,24 @@ class Asf extends Handler { + protected static $ASFIndexParametersObjectIndexSpecifiersIndexTypes = array( + 1 => 'Nearest Past Data Packet', + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint' + ); + + protected static $ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes = array( + 1 => 'Nearest Past Data Packet', + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint', + 0xFF => 'Frame Number Offset' + ); + + protected static $ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes = array( + 2 => 'Nearest Past Media Object', + 3 => 'Nearest Past Cleanpoint' + ); + /** * @param GetID3 $getid3 */ @@ -1581,8 +1599,9 @@ public static function KnownGUIDs() { 'GETID3_ASF_Audio_Media' => 'F8699E40-5B4D-11CF-A8FD-00805F5C442B', 'GETID3_ASF_Media_Object_Index_Object' => 'FEB103F8-12AD-4C64-840F-2A1D2F7AD48C', 'GETID3_ASF_Alt_Extended_Content_Encryption_Obj' => 'FF889EF1-ADEE-40DA-9E71-98704BB928CE', - 'GETID3_ASF_Index_Placeholder_Object' => 'D9AADE20-7C17-4F9C-BC28-8555DD98E2A2', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html - 'GETID3_ASF_Compatibility_Object' => '26F18B5D-4584-47EC-9F5F-0E651F0452C9', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html + 'GETID3_ASF_Index_Placeholder_Object' => 'D9AADE20-7C17-4F9C-BC28-8555DD98E2A2', // https://metacpan.org/dist/Audio-WMA/source/WMA.pm + 'GETID3_ASF_Compatibility_Object' => '26F18B5D-4584-47EC-9F5F-0E651F0452C9', // https://metacpan.org/dist/Audio-WMA/source/WMA.pm + 'GETID3_ASF_Media_Object_Index_Parameters_Object'=> '6B203BAD-3F11-48E4-ACA8-D7613DE2CFA7', ); return $GUIDarray; } @@ -1745,7 +1764,7 @@ public static function WMpictureTypeLookup($WMpictureType) { * @return array */ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_data, &$unhandled_sections) { - // http://msdn.microsoft.com/en-us/library/bb643323.aspx + // https://web.archive.org/web/20140419205228/http://msdn.microsoft.com/en-us/library/bb643323.aspx $offset = 0; $objectOffset = 0; @@ -1809,8 +1828,8 @@ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_dat $thisObject['stream_language_id_index'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; - $thisObject['average_time_per_frame'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; + $thisObject['average_time_per_frame'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 8)); + $offset += 8; $thisObject['stream_name_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; @@ -1827,7 +1846,7 @@ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_dat $streamName['stream_name_length'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); $offset += 2; - $streamName['stream_name'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $streamName['stream_name_length'])); + $streamName['stream_name'] = substr($asf_header_extension_object_data, $offset, $streamName['stream_name_length']); $offset += $streamName['stream_name_length']; $thisObject['stream_names'][$i] = $streamName; @@ -1849,7 +1868,7 @@ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_dat $payloadExtensionSystem['extension_system_info_length'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); $offset += 4; - $payloadExtensionSystem['extension_system_info_length'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $payloadExtensionSystem['extension_system_info_length'])); + $payloadExtensionSystem['extension_system_info'] = substr($asf_header_extension_object_data, $offset, $payloadExtensionSystem['extension_system_info_length']); $offset += $payloadExtensionSystem['extension_system_info_length']; $thisObject['payload_extension_systems'][$i] = $payloadExtensionSystem; @@ -1857,6 +1876,40 @@ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_dat break; + case GETID3_ASF_Advanced_Mutual_Exclusion_Object: + $thisObject['exclusion_type'] = substr($asf_header_extension_object_data, $offset, 16); + $offset += 16; + $thisObject['exclusion_type_text'] = $this->BytestringToGUID($thisObject['exclusion_type']); + + $thisObject['stream_numbers_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['stream_numbers_count']; $i++) { + $thisObject['stream_numbers'][$i] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + } + + break; + + case GETID3_ASF_Stream_Prioritization_Object: + $thisObject['priority_records_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['priority_records_count']; $i++) { + $priorityRecord = array(); + + $priorityRecord['stream_number'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $priorityRecord['flags_raw'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $priorityRecord['flags']['mandatory'] = (bool) $priorityRecord['flags_raw'] & 0x00000001; + + $thisObject['priority_records'][$i] = $priorityRecord; + } + + break; + case GETID3_ASF_Padding_Object: // padding, skip it break; @@ -1974,6 +2027,103 @@ public function HeaderExtensionObjectDataParse(&$asf_header_extension_object_dat } break; + case GETID3_ASF_Index_Parameters_Object: + $thisObject['index_entry_time_interval'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Media_Object_Index_Parameters_Object: + $thisObject['index_entry_count_interval'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFMediaObjectIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Timecode_Index_Parameters_Object: + // 4.11 Timecode Index Parameters Object (mandatory only if TIMECODE index is present in file, 0 or 1) + // Field name Field type Size (bits) + // Object ID GUID 128 // GUID for the Timecode Index Parameters Object - ASF_Timecode_Index_Parameters_Object + // Object Size QWORD 64 // Specifies the size, in bytes, of the Timecode Index Parameters Object. Valid values are at least 34 bytes. + // Index Entry Count Interval DWORD 32 // This value is ignored for the Timecode Index Parameters Object. + // Index Specifiers Count WORD 16 // Specifies the number of entries in the Index Specifiers list. Valid values are 1 and greater. + // Index Specifiers array of: varies // + // * Stream Number WORD 16 // Specifies the stream number that the Index Specifiers refer to. Valid values are between 1 and 127. + // * Index Type WORD 16 // Specifies the type of index. Values are defined as follows (1 is not a valid value): + // 2 = Nearest Past Media Object - indexes point to the closest data packet containing an entire video frame or the first fragment of a video frame + // 3 = Nearest Past Cleanpoint - indexes point to the closest data packet containing an entire video frame (or first fragment of a video frame) that is a key frame. + // Nearest Past Media Object is the most common value + + $thisObject['index_entry_count_interval'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); + $offset += 4; + + $thisObject['index_specifiers_count'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + for ($i = 0; $i < $thisObject['index_specifiers_count']; $i++) { + $indexSpecifier = array(); + + $indexSpecifier['stream_number'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + + $indexSpecifier['index_type'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); + $offset += 2; + $indexSpecifier['index_type_text'] = isset(static::$ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']]) + ? static::$ASFTimecodeIndexParametersObjectIndexSpecifiersIndexTypes[$indexSpecifier['index_type']] + : 'invalid' + ; + + $thisObject['index_specifiers'][$i] = $indexSpecifier; + } + + break; + + case GETID3_ASF_Compatibility_Object: + $thisObject['profile'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 1)); + $offset += 1; + + $thisObject['mode'] = Utils::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 1)); + $offset += 1; + + break; + default: $unhandled_sections++; if ($this->GUIDname($thisObject['guid_text'])) { diff --git a/src/Module/AudioVideo/Mpeg.php b/src/Module/AudioVideo/Mpeg.php index 7b92c796..0782aafa 100644 --- a/src/Module/AudioVideo/Mpeg.php +++ b/src/Module/AudioVideo/Mpeg.php @@ -615,7 +615,7 @@ public static function videoAspectRatioLookup($rawaspectratio, $mpeg_version=1, 2 => array(0, 1, 1.3333, 1.7778, 2.2100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), ); $ratio = (float) (isset($lookup[$mpeg_version][$rawaspectratio]) ? $lookup[$mpeg_version][$rawaspectratio] : 0); - if ($mpeg_version == 2 && $ratio != 1) { + if ($mpeg_version == 2 && $ratio != 1 && $width != 0) { // Calculate pixel aspect ratio from MPEG-2 display aspect ratio $ratio = $ratio * $height / $width; } diff --git a/src/Module/AudioVideo/QuickTime.php b/src/Module/AudioVideo/QuickTime.php index 450e988c..e730dc9e 100644 --- a/src/Module/AudioVideo/QuickTime.php +++ b/src/Module/AudioVideo/QuickTime.php @@ -63,12 +63,16 @@ public function Analyze() { $this->fseek($offset); $AtomHeader = $this->fread(8); + // https://github.com/JamesHeinrich/getID3/issues/382 + // Atom sizes are stored as 32-bit number in most cases, but sometimes (notably for "mdat") + // a 64-bit value is required, in which case the normal 32-bit size field is set to 0x00000001 + // and the 64-bit "real" size value is the next 8 bytes. + $atom_size_extended_bytes = 0; $atomsize = Utils::BigEndian2Int(substr($AtomHeader, 0, 4)); $atomname = substr($AtomHeader, 4, 4); - - // 64-bit MOV patch by jlegateØktnc*com if ($atomsize == 1) { - $atomsize = Utils::BigEndian2Int($this->fread(8)); + $atom_size_extended_bytes = 8; + $atomsize = Utils::BigEndian2Int($this->fread($atom_size_extended_bytes)); } if (($offset + $atomsize) > $info['avdataend']) { @@ -87,12 +91,14 @@ public function Analyze() { $info['quicktime'][$atomname]['offset'] = $offset; break; } - $atomHierarchy = array(); - $parsedAtomData = $this->QuicktimeParseAtom($atomname, $atomsize, $this->fread(min($atomsize, $atom_data_read_buffer_size)), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); + $parsedAtomData = $this->QuicktimeParseAtom($atomname, $atomsize, $this->fread(min($atomsize - $atom_size_extended_bytes, $atom_data_read_buffer_size)), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); $parsedAtomData['name'] = $atomname; $parsedAtomData['size'] = $atomsize; $parsedAtomData['offset'] = $offset; + if ($atom_size_extended_bytes) { + $parsedAtomData['xsize_bytes'] = $atom_size_extended_bytes; + } if (in_array($atomname, array('uuid'))) { @$info['quicktime'][$atomname][] = $parsedAtomData; } else { @@ -260,7 +266,9 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset } else { switch ($atomname) { case 'moov': // MOVie container atom + case 'moof': // MOvie Fragment box case 'trak': // TRAcK container atom + case 'traf': // TRAck Fragment box case 'clip': // CLIPping container atom case 'matt': // track MATTe container atom case 'edts': // EDiTS container atom @@ -844,6 +852,7 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset case 'dvcp': case 'gif ': case 'h263': + case 'hvc1': case 'jpeg': case 'kpcd': case 'mjpa': @@ -1508,7 +1517,7 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $OldAVDataEnd = $info['avdataend']; $info['avdataend'] = $atom_structure['offset'] + $atom_structure['size']; // $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size']; - $getid3_temp = new getID3(); + $getid3_temp = new GetID3(); $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; $getid3_temp->info['avdataend'] = $info['avdataend']; @@ -1542,6 +1551,19 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset unset($mdat_offset, $chapter_string_length, $chapter_matches); break; + case 'ID32': // ID3v2 + $getid3_temp = new GetID3(); + $getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp); + $getid3_id3v2 = new ID3v2($getid3_temp); + $getid3_id3v2->StartingOffset = $atom_structure['offset'] + 14; // framelength(4)+framename(4)+flags(4)+??(2) + if ($atom_structure['valid'] = $getid3_id3v2->Analyze()) { + $atom_structure['id3v2'] = $getid3_temp->info['id3v2']; + } else { + $this->warning('ID32 frame at offset '.$atom_structure['offset'].' did not parse'); + } + unset($getid3_temp, $getid3_id3v2); + break; + case 'free': // FREE space atom case 'skip': // SKIP atom case 'wide': // 64-bit expansion placeholder atom @@ -1700,7 +1722,8 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $atom_structure['language'] = substr($atom_data, 4 + 0, 2); $atom_structure['unknown'] = Utils::BigEndian2Int(substr($atom_data, 4 + 2, 2)); $atom_structure['data'] = substr($atom_data, 4 + 4); - $atom_structure['key_name'] = @$info['quicktime']['temp_meta_key_names'][$metaDATAkey++]; + $atom_structure['key_name'] = (isset($info['quicktime']['temp_meta_key_names'][$metaDATAkey]) ? $info['quicktime']['temp_meta_key_names'][$metaDATAkey] : ''); + $metaDATAkey++; if ($atom_structure['key_name'] && $atom_structure['data']) { @$info['quicktime']['comments'][str_replace('com.apple.quicktime.', '', $atom_structure['key_name'])][] = $atom_structure['data']; @@ -2075,6 +2098,28 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $atom_structure['track_number'] = Utils::BigEndian2Int($atom_data); break; + +// AVIF-related - https://docs.rs/avif-parse/0.13.2/src/avif_parse/boxes.rs.html + case 'pitm': // Primary ITeM + case 'iloc': // Item LOCation + case 'iinf': // Item INFo + case 'iref': // Image REFerence + case 'iprp': // Image PRoPerties +$this->error('AVIF files not currently supported'); + $atom_structure['data'] = $atom_data; + break; + + case 'tfdt': // Track Fragment base media Decode Time box + case 'tfhd': // Track Fragment HeaDer box + case 'mfhd': // Movie Fragment HeaDer box + case 'trun': // Track fragment RUN box +$this->error('fragmented mp4 files not currently supported'); + $atom_structure['data'] = $atom_data; + break; + + case 'mvex': // MoVie EXtends box + case 'pssh': // Protection System Specific Header box + case 'sidx': // Segment InDeX box default: $this->warning('Unknown QuickTime atom type: "'.preg_replace('#[^a-zA-Z0-9 _\\-]#', '?', $atomname).'" ('.trim(Utils::PrintHexBytes($atomname)).'), '.$atomsize.' bytes at offset '.$baseoffset); $atom_structure['data'] = $atom_data; @@ -2323,6 +2368,7 @@ public function QuicktimeVideoCodecLookup($codecid) { $QuicktimeVideoCodecLookup['gif '] = 'GIF'; $QuicktimeVideoCodecLookup['h261'] = 'H261'; $QuicktimeVideoCodecLookup['h263'] = 'H263'; + $QuicktimeVideoCodecLookup['hvc1'] = 'H.265/HEVC'; $QuicktimeVideoCodecLookup['IV41'] = 'Indeo4'; $QuicktimeVideoCodecLookup['jpeg'] = 'JPEG'; $QuicktimeVideoCodecLookup['kpcd'] = 'PhotoCD'; diff --git a/src/Module/AudioVideo/Riff.php b/src/Module/AudioVideo/Riff.php index 0d729b37..541b00fb 100644 --- a/src/Module/AudioVideo/Riff.php +++ b/src/Module/AudioVideo/Riff.php @@ -2158,7 +2158,7 @@ public static function ParseBITMAPINFOHEADER($BITMAPINFOHEADER, $littleEndian=tr $parsed['biYPelsPerMeter'] = substr($BITMAPINFOHEADER, 28, 4); // vertical resolution, in pixels per metre, of the target device $parsed['biClrUsed'] = substr($BITMAPINFOHEADER, 32, 4); // actual number of color indices in the color table used by the bitmap. If this value is zero, the bitmap uses the maximum number of colors corresponding to the value of the biBitCount member for the compression mode specified by biCompression $parsed['biClrImportant'] = substr($BITMAPINFOHEADER, 36, 4); // number of color indices that are considered important for displaying the bitmap. If this value is zero, all colors are important - $parsed = array_map('Utils::'.($littleEndian ? 'Little' : 'Big').'Endian2Int', $parsed); + $parsed = array_map('JamesHeinrich\\GetID3\\Utils::'.($littleEndian ? 'Little' : 'Big').'Endian2Int', $parsed); $parsed['fourcc'] = substr($BITMAPINFOHEADER, 16, 4); // compression identifier diff --git a/src/Module/Graphic/Gif.php b/src/Module/Graphic/Gif.php index 4fb8895c..74198a79 100644 --- a/src/Module/Graphic/Gif.php +++ b/src/Module/Graphic/Gif.php @@ -21,6 +21,7 @@ /** * @link https://www.w3.org/Graphics/GIF/spec-gif89a.txt * @link http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + * @link http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension */ class Gif extends Handler { @@ -167,14 +168,31 @@ public function Analyze() { $ExtensionBlock['byte_length'] = Utils::LittleEndian2Int(substr($ExtensionBlockData, 2, 1)); $ExtensionBlock['data'] = (($ExtensionBlock['byte_length'] > 0) ? $this->fread($ExtensionBlock['byte_length']) : null); - if (substr($ExtensionBlock['data'], 0, 11) == 'NETSCAPE2.0') { // Netscape Application Block (NAB) - $ExtensionBlock['data'] .= $this->fread(4); - if (substr($ExtensionBlock['data'], 11, 2) == "\x03\x01") { - $info['gif']['animation']['animated'] = true; - $info['gif']['animation']['loop_count'] = Utils::LittleEndian2Int(substr($ExtensionBlock['data'], 13, 2)); - } else { - $this->warning('Expecting 03 01 at offset '.($this->ftell() - 4).', found "'.Utils::PrintHexBytes(substr($ExtensionBlock['data'], 11, 2)).'"'); - } + switch ($ExtensionBlock['function_code']) { + case 0xFF: + // Application Extension + if ($ExtensionBlock['byte_length'] != 11) { + $this->warning('Expected block size of the Application Extension is 11 bytes, found '.$ExtensionBlock['byte_length'].' at offset '.$this->ftell()); + break; + } + + if (substr($ExtensionBlock['data'], 0, 11) !== 'NETSCAPE2.0' + && substr($ExtensionBlock['data'], 0, 11) !== 'ANIMEXTS1.0' + ) { + $this->warning('Ignoring unsupported Application Extension '.substr($ExtensionBlock['data'], 0, 11)); + break; + } + + // Netscape Application Block (NAB) + $ExtensionBlock['data'] .= $this->fread(4); + if (substr($ExtensionBlock['data'], 11, 2) == "\x03\x01") { + $info['gif']['animation']['animated'] = true; + $info['gif']['animation']['loop_count'] = Utils::LittleEndian2Int(substr($ExtensionBlock['data'], 13, 2)); + } else { + $this->warning('Expecting 03 01 at offset '.($this->ftell() - 4).', found "'.Utils::PrintHexBytes(substr($ExtensionBlock['data'], 11, 2)).'"'); + } + + break; } if ($this->getid3->option_extra_info) { diff --git a/src/Module/Misc/Cue.php b/src/Module/Misc/Cue.php index 8b1c3d0a..7f2acc89 100644 --- a/src/Module/Misc/Cue.php +++ b/src/Module/Misc/Cue.php @@ -70,7 +70,7 @@ public function readCueSheetFilename($filename) public function readCueSheet(&$filedata) { $cue_lines = array(); - foreach (explode("\n", str_replace("\r", null, $filedata)) as $line) + foreach (explode("\n", str_replace("\r", '', $filedata)) as $line) { if ( (strlen($line) > 0) && ($line[0] != '#')) { diff --git a/src/Utils.php b/src/Utils.php index 36d847be..3bee5b86 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -888,7 +888,7 @@ public static function XML2array($XMLstring) { // 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); + $XMLobject = simplexml_load_string($XMLstring, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_NONET | LIBXML_NOWARNING | LIBXML_COMPACT); $return = self::SimpleXMLelement2array($XMLobject); @libxml_disable_entity_loader($loader); return $return;