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/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..f5e194d2 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': @@ -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/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;