diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dbeea195..7b5543a7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -21,8 +21,9 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v3" - uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" @@ -36,11 +37,11 @@ jobs: name: "Static Analysis" runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v3" - uses: "shivammathur/setup-php@v2" with: php-version: "7.4" - tools: "phpstan:0.12.99" + tools: "phpstan:1.8.11" coverage: "none" - uses: "ramsey/composer-install@v2" - name: "Run PHPStan" diff --git a/LICENSE.md b/LICENSE.md index 4e5f0658..0bdeea29 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -24,4 +24,4 @@ Mozilla MPL getID3 Commercial License ------------------------- -* [gCL](http://getid3.org/#gCL) (payment required) +* [gCL](http://getid3.org/#gCL) (no longer available, existing licenses remain valid) diff --git a/changelog.txt b/changelog.txt index e6a6e586..488e4c1d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,19 @@ Version History =============== +1.9.23: [2023-10-19] James Heinrich :: 1.9.23-202310190849 + ยป add detection support for 7-zip archives + * #424 RIFF Undefined index "data" + * #421 tag.xmp remove GLOBALS + * #419 Quicktime Undefined index "time_scale" + * #418 tag.xmp zero-length fread + * #414 Quicktime bitrate for mp4 audio + * #413 Quicktime audio metadata + * #410 MPEG-1 pixel aspect ratio + * #407 PHP 8.1 compatibility + * #404 guard against division by zero + * #402 remove utf8_encode/utf8_decode + 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 diff --git a/demos/demo.browse.php b/demos/demo.browse.php index 4a1739dd..eb4cb1e1 100644 --- a/demos/demo.browse.php +++ b/demos/demo.browse.php @@ -460,7 +460,7 @@ function table_var_dump($variable, $wrap_in_td=false, $encoding='') { global $FileSystemEncoding; $encoding = ($encoding ? $encoding : $FileSystemEncoding); $returnstring = ''; - switch (gettype($variable)) { + switch (strtolower(gettype($variable))) { case 'array': $returnstring .= ($wrap_in_td ? '' : ''); $returnstring .= ''; diff --git a/demos/demo.mp3header.php b/demos/demo.mp3header.php index 222fe40e..57c241c8 100644 --- a/demos/demo.mp3header.php +++ b/demos/demo.mp3header.php @@ -705,7 +705,7 @@ function RoughTranslateUnicodeToASCII($rawdata, $frame_textencoding) { break; case 3: // UTF-8 encoded Unicode. Terminated with $00. - $asciidata = utf8_decode($rawdata); + $asciidata = utf8_to_iso8859_1($rawdata); break; case 255: // Unicode, Big-Endian. Terminated with $00 00. @@ -929,43 +929,40 @@ function image_type_to_mime_type($imagetypeid) { } } -if (!function_exists('utf8_decode')) { - // PHP has this function built-in if it's configured with the --with-xml option - // This version of the function is only provided in case XML isn't installed - function utf8_decode($utf8text) { - // http://www.php.net/manual/en/function.utf8-encode.php - // bytes bits representation - // 1 7 0bbbbbbb - // 2 11 110bbbbb 10bbbbbb - // 3 16 1110bbbb 10bbbbbb 10bbbbbb - // 4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - - $utf8length = strlen($utf8text); - $decodedtext = ''; - for ($i = 0; $i < $utf8length; $i++) { - if ((ord($utf8text[$i]) & 0x80) == 0) { - $decodedtext .= $utf8text[$i]; - } elseif ((ord($utf8text[$i]) & 0xF0) == 0xF0) { - $decodedtext .= '?'; - $i += 3; - } elseif ((ord($utf8text[$i]) & 0xE0) == 0xE0) { +function utf8_to_iso8859_1($utf8text) { + // http://www.php.net/manual/en/function.utf8-encode.php + // bytes bits representation + // 1 7 0bbbbbbb + // 2 11 110bbbbb 10bbbbbb + // 3 16 1110bbbb 10bbbbbb 10bbbbbb + // 4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb + + $utf8length = strlen($utf8text); + $decodedtext = ''; + for ($i = 0; $i < $utf8length; $i++) { + if ((ord($utf8text[$i]) & 0x80) == 0) { + $decodedtext .= $utf8text[$i]; + } elseif ((ord($utf8text[$i]) & 0xF0) == 0xF0) { + $decodedtext .= '?'; + $i += 3; + } elseif ((ord($utf8text[$i]) & 0xE0) == 0xE0) { + $decodedtext .= '?'; + $i += 2; + } elseif ((ord($utf8text[$i]) & 0xC0) == 0xC0) { + // 2 11 110bbbbb 10bbbbbb + $decodedchar = Bin2Dec(substr(Dec2Bin(ord($utf8text[$i])), 3, 5).substr(Dec2Bin(ord($utf8text[($i + 1)])), 2, 6)); + if ($decodedchar <= 255) { + $decodedtext .= chr($decodedchar); + } else { $decodedtext .= '?'; - $i += 2; - } elseif ((ord($utf8text[$i]) & 0xC0) == 0xC0) { - // 2 11 110bbbbb 10bbbbbb - $decodedchar = Bin2Dec(substr(Dec2Bin(ord($utf8text[$i])), 3, 5).substr(Dec2Bin(ord($utf8text[($i + 1)])), 2, 6)); - if ($decodedchar <= 255) { - $decodedtext .= chr($decodedchar); - } else { - $decodedtext .= '?'; - } - $i += 1; } + $i += 1; } - return $decodedtext; } + return $decodedtext; } + if (!function_exists('DateMac2Unix')) { function DateMac2Unix($macdate) { // Macintosh timestamp: seconds since 00:00h January 1, 1904 diff --git a/readme.txt b/readme.txt index 1f79d75d..6d40e1cf 100644 --- a/readme.txt +++ b/readme.txt @@ -20,7 +20,8 @@ GNU LGPL: https://gnu.org/licenses/lgpl.html (v3) Mozilla MPL: https://www.mozilla.org/MPL/2.0/ (v2) -getID3 Commercial License: https://www.getid3.org/#gCL (payment required) +getID3 Commercial License: https://www.getid3.org/#gCL +(no longer available, existing licenses remain valid) ***************************************************************** ***************************************************************** diff --git a/src/GetID3.php b/src/GetID3.php index 5b884919..13b0ddef 100644 --- a/src/GetID3.php +++ b/src/GetID3.php @@ -319,7 +319,7 @@ class GetID3 */ protected $startup_warning = ''; - const VERSION = '2.0.x-202207161647'; + const VERSION = '2.0.x-202310190849'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; @@ -1253,6 +1253,15 @@ public function GetFileFormatArray() { 'fail_ape' => 'ERROR', ), + // XZ - data - XZ compressed data + '7zip' => array( + 'pattern' => '^7z\\xBC\\xAF\\x27\\x1C', + 'module' => 'Archive\\SevenZip', + 'mime_type' => 'application/x-7z-compressed', + 'fail_id3' => 'ERROR', + 'fail_ape' => 'ERROR', + ), + // Misc other formats @@ -1750,7 +1759,7 @@ public function CalculateCompressionRatioVideo() { } $BitrateUncompressed = $this->info['video']['resolution_x'] * $this->info['video']['resolution_y'] * $this->info['video']['bits_per_sample'] * $FrameRate; - $this->info['video']['compression_ratio'] = $BitrateCompressed / $BitrateUncompressed; + $this->info['video']['compression_ratio'] = Utils::SafeDiv($BitrateCompressed, $BitrateUncompressed, 1); return true; } diff --git a/src/Module/Archive/Hpk.php b/src/Module/Archive/Hpk.php index b72e3e85..b223fbcb 100644 --- a/src/Module/Archive/Hpk.php +++ b/src/Module/Archive/Hpk.php @@ -44,7 +44,7 @@ public function Analyze() { $info['hpk']['header']['fragmented_filesystem_offset'] = Utils::LittleEndian2Int(substr($HPKheader, 28, 4)); $info['hpk']['header']['fragmented_filesystem_length'] = Utils::LittleEndian2Int(substr($HPKheader, 32, 4)); - $info['hpk']['header']['filesystem_entries'] = $info['hpk']['header']['fragmented_filesystem_length'] / ($info['hpk']['header']['fragments_per_file'] * 8); + $info['hpk']['header']['filesystem_entries'] = Utils::SafeDiv($info['hpk']['header']['fragmented_filesystem_length'], $info['hpk']['header']['fragments_per_file'] * 8); $this->fseek($info['hpk']['header']['fragmented_filesystem_offset']); for ($i = 0; $i < $info['hpk']['header']['filesystem_entries']; $i++) { $offset = Utils::LittleEndian2Int($this->fread(4)); diff --git a/src/Module/Archive/SevenZip.php b/src/Module/Archive/SevenZip.php new file mode 100644 index 00000000..22120846 --- /dev/null +++ b/src/Module/Archive/SevenZip.php @@ -0,0 +1,52 @@ + // +// available at https://github.com/JamesHeinrich/getID3 // +// or https://www.getid3.org // +// or http://getid3.sourceforge.net // +// see readme.txt for more details // +///////////////////////////////////////////////////////////////// +// // +// module.archive.7zip.php // +// module for analyzing 7zip files // +// /// +///////////////////////////////////////////////////////////////// + +class SevenZip extends Handler +{ + /** + * @return bool + */ + public function Analyze() { + $info = &$this->getid3->info; + + $this->fseek($info['avdataoffset']); + $z7header = $this->fread(32); + + // https://py7zr.readthedocs.io/en/latest/archive_format.html + $info['7zip']['header']['magic'] = substr($z7header, 0, 6); + if ($info['7zip']['header']['magic'] != '7z'."\xBC\xAF\x27\x1C") { + $this->error('Invalid 7zip stream header magic (expecting 37 7A BC AF 27 1C, found '.Utils::PrintHexBytes($info['7zip']['header']['magic']).') at offset '.$info['avdataoffset']); + return false; + } + $info['fileformat'] = '7zip'; + + $info['7zip']['header']['version_major'] = Utils::LittleEndian2Int(substr($z7header, 6, 1)); // always 0x00 (?) + $info['7zip']['header']['version_minor'] = Utils::LittleEndian2Int(substr($z7header, 7, 1)); // always 0x04 (?) + $info['7zip']['header']['start_header_crc'] = Utils::LittleEndian2Int(substr($z7header, 8, 4)); + $info['7zip']['header']['next_header_offset'] = Utils::LittleEndian2Int(substr($z7header, 12, 8)); + $info['7zip']['header']['next_header_size'] = Utils::LittleEndian2Int(substr($z7header, 20, 8)); + $info['7zip']['header']['next_header_crc'] = Utils::LittleEndian2Int(substr($z7header, 28, 4)); + +$this->error('7zip parsing not enabled in this version of getID3() ['.$this->getid3->version().']'); + return false; + + } + +} diff --git a/src/Module/Audio/Amr.php b/src/Module/Audio/Amr.php index 873742bf..f517505d 100644 --- a/src/Module/Audio/Amr.php +++ b/src/Module/Audio/Amr.php @@ -63,7 +63,7 @@ public function Analyze() { } while (strlen($buffer) > 0); $info['playtime_seconds'] = array_sum($thisfile_amr['frame_mode_count']) * 0.020; // each frame contain 160 samples and is 20 milliseconds long - $info['audio']['bitrate'] = (8 * ($info['avdataend'] - $info['avdataoffset'])) / $info['playtime_seconds']; // bitrate could be calculated from average bitrate by distributation of frame types. That would give effective audio bitrate, this gives overall file bitrate which will be a little bit higher since every frame will waste 8 bits for header, plus a few bits for octet padding + $info['audio']['bitrate'] = Utils::SafeDiv(8 * ($info['avdataend'] - $info['avdataoffset']), $info['playtime_seconds']); // bitrate could be calculated from average bitrate by distributation of frame types. That would give effective audio bitrate, this gives overall file bitrate which will be a little bit higher since every frame will waste 8 bits for header, plus a few bits for octet padding $info['bitrate'] = $info['audio']['bitrate']; return true; diff --git a/src/Module/Audio/Au.php b/src/Module/Audio/Au.php index 8632c85b..8ce73489 100644 --- a/src/Module/Audio/Au.php +++ b/src/Module/Audio/Au.php @@ -69,8 +69,8 @@ public function Analyze() { $this->warning('Possible truncated file - expecting "'.$thisfile_au['data_size'].'" bytes of audio data, only found '.($info['avdataend'] - $info['avdataoffset']).' bytes"'); } - $info['playtime_seconds'] = $thisfile_au['data_size'] / ($thisfile_au['sample_rate'] * $thisfile_au['channels'] * ($thisfile_au['used_bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($thisfile_au['data_size'] * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = $thisfile_au['sample_rate'] * $thisfile_au['channels'] * $thisfile_au['used_bits_per_sample']; + $info['playtime_seconds'] = Utils::SafeDiv($thisfile_au['data_size'], $info['audio']['bitrate'] / 8); return true; } diff --git a/src/Module/Audio/Avr.php b/src/Module/Audio/Avr.php index 8de0b52e..83ed8c34 100644 --- a/src/Module/Audio/Avr.php +++ b/src/Module/Audio/Avr.php @@ -120,9 +120,9 @@ public function Analyze() { $info['audio']['bits_per_sample'] = $info['avr']['bits_per_sample']; $info['audio']['sample_rate'] = $info['avr']['sample_rate']; $info['audio']['channels'] = ($info['avr']['flags']['stereo'] ? 2 : 1); - $info['playtime_seconds'] = ($info['avr']['sample_length'] / $info['audio']['channels']) / $info['avr']['sample_rate']; - $info['audio']['bitrate'] = ($info['avr']['sample_length'] * (($info['avr']['bits_per_sample'] == 8) ? 8 : 16)) / $info['playtime_seconds']; - + $bits_per_sample = ($info['avr']['bits_per_sample'] == 8) ? 8 : 16; + $info['audio']['bitrate'] = $bits_per_sample * $info['audio']['channels'] * $info['avr']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['avr']['sample_length'] * $bits_per_sample, $info['audio']['bitrate']); return true; } diff --git a/src/Module/Audio/Bonk.php b/src/Module/Audio/Bonk.php index 2be7a141..62bb0f6f 100644 --- a/src/Module/Audio/Bonk.php +++ b/src/Module/Audio/Bonk.php @@ -156,7 +156,7 @@ public function HandleBonkTags($BonkTagName) { $info['audio']['lossless'] = $thisfile_bonk_BONK['lossless']; $info['audio']['codec'] = 'bonk'; - $info['playtime_seconds'] = $thisfile_bonk_BONK['number_samples'] / ($thisfile_bonk_BONK['sample_rate'] * $thisfile_bonk_BONK['channels']); + $info['playtime_seconds'] = Utils::SafeDiv($thisfile_bonk_BONK['number_samples'], $thisfile_bonk_BONK['sample_rate'] * $thisfile_bonk_BONK['channels']); if ($info['playtime_seconds'] > 0) { $info['audio']['bitrate'] = (($info['bonk']['dataend'] - $info['bonk']['dataoffset']) * 8) / $info['playtime_seconds']; } diff --git a/src/Module/Audio/Dsdiff.php b/src/Module/Audio/Dsdiff.php index 6c37db2e..01a8f340 100644 --- a/src/Module/Audio/Dsdiff.php +++ b/src/Module/Audio/Dsdiff.php @@ -198,9 +198,13 @@ public function Analyze() { $this->fseek(1, SEEK_CUR); } - if ($commentkey = (($thisChunk['name'] == 'DIAR') ? 'artist' : (($thisChunk['name'] == 'DITI') ? 'title' : ''))) { - @$info['dsdiff']['comments'][$commentkey][] = $thisChunk['description']; - } + $commentKeys = array( + 'DIAR' => 'artist', + 'DITI' => 'title' + ); + $commentkey = $commentKeys[$thisChunk['name']]; + + $info['dsdiff']['comments'][$commentkey][] = $thisChunk['description']; break; case 'EMID': // Edited Master ID chunk if ($thisChunk['size']) { diff --git a/src/Module/Audio/Dsf.php b/src/Module/Audio/Dsf.php index 215c320b..20bdb6aa 100644 --- a/src/Module/Audio/Dsf.php +++ b/src/Module/Audio/Dsf.php @@ -118,7 +118,7 @@ public function Analyze() { $info['audio']['sample_rate'] = $info['dsf']['fmt']['sample_rate']; $info['audio']['channels'] = $info['dsf']['fmt']['channels']; $info['audio']['bitrate'] = $info['audio']['bits_per_sample'] * $info['audio']['sample_rate'] * $info['audio']['channels']; - $info['playtime_seconds'] = ($info['dsf']['data']['data_chunk_size'] * 8) / $info['audio']['bitrate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['dsf']['data']['data_chunk_size'] * 8, $info['audio']['bitrate']); return true; } diff --git a/src/Module/Audio/Lpac.php b/src/Module/Audio/Lpac.php index f2392e50..89c895a8 100644 --- a/src/Module/Audio/Lpac.php +++ b/src/Module/Audio/Lpac.php @@ -127,8 +127,8 @@ public function Analyze() { } } - $info['playtime_seconds'] = $info['lpac']['total_samples'] / $info['audio']['sample_rate']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['playtime_seconds'] = Utils::SafeDiv($info['lpac']['total_samples'], $info['audio']['sample_rate']); + $info['audio']['bitrate'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); return true; } diff --git a/src/Module/Audio/Midi.php b/src/Module/Audio/Midi.php index 17b5756e..aeb85afa 100644 --- a/src/Module/Audio/Midi.php +++ b/src/Module/Audio/Midi.php @@ -106,7 +106,6 @@ public function Analyze() { $thisfile_midi['totalticks'] = 0; $info['playtime_seconds'] = 0; $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat - $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat $MicroSecondsPerQuarterNoteAfter = array (); $MIDIevents = array(); @@ -251,7 +250,6 @@ public function Analyze() { return false; } $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat; - $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60; $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat; $TicksAtCurrentBPM = 0; break; diff --git a/src/Module/Audio/Mp3.php b/src/Module/Audio/Mp3.php index 2f565917..c64ebe61 100644 --- a/src/Module/Audio/Mp3.php +++ b/src/Module/Audio/Mp3.php @@ -1380,11 +1380,11 @@ public function getOnlyMPEGaudioInfoBruteForce() { $Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])] = isset($Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])]) ? ++$Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])] : 1; $Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]] = isset($Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]]) ? ++$Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]] : 1; if (++$frames_scanned >= $max_frames_scan) { - $pct_data_scanned = ($this->ftell() - $info['avdataoffset']) / ($info['avdataend'] - $info['avdataoffset']); + $pct_data_scanned = Utils::SafeDiv($this->ftell() - $info['avdataoffset'], $info['avdataend'] - $info['avdataoffset']); $this->warning('too many MPEG audio frames to scan, only scanned first '.$max_frames_scan.' frames ('.number_format($pct_data_scanned * 100, 1).'% of file) and extrapolated distribution, playtime and bitrate may be incorrect.'); foreach ($Distribution as $key1 => $value1) { foreach ($value1 as $key2 => $value2) { - $Distribution[$key1][$key2] = round($value2 / $pct_data_scanned); + $Distribution[$key1][$key2] = $pct_data_scanned ? round($value2 / $pct_data_scanned) : 1; } } break; @@ -1475,7 +1475,7 @@ public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { $SyncSeekAttemptsMax = 1000; $FirstFrameThisfileInfo = null; while ($SynchSeekOffset < $sync_seek_buffer_size) { - if ((($avdataoffset + $SynchSeekOffset) < $info['avdataend']) && !feof($this->getid3->fp)) { + if ((($avdataoffset + $SynchSeekOffset) < $info['avdataend']) && !$this->feof()) { if ($SynchSeekOffset > $sync_seek_buffer_size) { // if a synch's not found within the first 128k bytes, then give up @@ -1490,20 +1490,6 @@ public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { unset($info['mpeg']); } return false; - - } elseif (feof($this->getid3->fp)) { - - $this->error('Could not find valid MPEG audio synch before end of file'); - if (isset($info['audio']['bitrate'])) { - unset($info['audio']['bitrate']); - } - if (isset($info['mpeg']['audio'])) { - unset($info['mpeg']['audio']); - } - if (isset($info['mpeg']) && (!is_array($info['mpeg']) || (count($info['mpeg']) == 0))) { - unset($info['mpeg']); - } - return false; } } @@ -1652,7 +1638,7 @@ public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { } $frames_scanned++; if ($frames_scan_per_segment && (++$frames_scanned_this_segment >= $frames_scan_per_segment)) { - $this_pct_scanned = ($this->ftell() - $scan_start_offset[$current_segment]) / ($info['avdataend'] - $info['avdataoffset']); + $this_pct_scanned = Utils::SafeDiv($this->ftell() - $scan_start_offset[$current_segment], $info['avdataend'] - $info['avdataoffset']); if (($current_segment == 0) && (($this_pct_scanned * $max_scan_segments) >= 1)) { // file likely contains < $max_frames_scan, just scan as one segment $max_scan_segments = 1; @@ -1743,6 +1729,10 @@ public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { } $info['audio']['channels'] = $info['mpeg']['audio']['channels']; + if ($info['audio']['channels'] < 1) { + $this->error('Corrupt MP3 file: no channels'); + return false; + } $info['audio']['channelmode'] = $info['mpeg']['audio']['channelmode']; $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; return true; diff --git a/src/Module/Audio/Mpc.php b/src/Module/Audio/Mpc.php index c6dc0be6..9afff8a8 100644 --- a/src/Module/Audio/Mpc.php +++ b/src/Module/Audio/Mpc.php @@ -137,7 +137,7 @@ public function ParseMPCsv8() { $info['audio']['channels'] = $thisPacket['channels']; $info['audio']['sample_rate'] = $thisPacket['sample_frequency']; $info['playtime_seconds'] = $thisPacket['sample_count'] / $thisPacket['sample_frequency']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); break; case 'RG': // Replay Gain @@ -282,7 +282,7 @@ public function ParseMPCsv7() { $info['audio']['sample_rate'] = $thisfile_mpc_header['sample_rate']; $thisfile_mpc_header['samples'] = ((($thisfile_mpc_header['frame_count'] - 1) * 1152) + $thisfile_mpc_header['last_frame_length']) * $info['audio']['channels']; - $info['playtime_seconds'] = ($thisfile_mpc_header['samples'] / $info['audio']['channels']) / $info['audio']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($thisfile_mpc_header['samples'], $info['audio']['channels'] * $info['audio']['sample_rate']); if ($info['playtime_seconds'] == 0) { $this->error('Corrupt MPC file: playtime_seconds == zero'); return false; diff --git a/src/Module/Audio/Ogg.php b/src/Module/Audio/Ogg.php index 9cfd9c4e..23a1cd62 100644 --- a/src/Module/Audio/Ogg.php +++ b/src/Module/Audio/Ogg.php @@ -209,8 +209,8 @@ public function Analyze() { $filedataoffset += 20; $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor']; - $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']; - $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']; + $info['ogg']['skeleton']['fishead']['presentationtime'] = Utils::SafeDiv($info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'], $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']); + $info['ogg']['skeleton']['fishead']['basetime'] = Utils::SafeDiv($info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'], $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']); $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc']; @@ -287,7 +287,7 @@ public function Analyze() { $info['audio']['sample_rate'] = $info['flac']['STREAMINFO']['sample_rate']; $info['audio']['channels'] = $info['flac']['STREAMINFO']['channels']; $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample']; - $info['playtime_seconds'] = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['flac']['STREAMINFO']['samples_stream'], $info['flac']['STREAMINFO']['sample_rate']); } } else { @@ -358,7 +358,7 @@ public function Analyze() { return false; } if (!empty($info['audio']['sample_rate'])) { - $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']); + $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) * $info['audio']['sample_rate'] / $info['ogg']['samples']; } } @@ -533,12 +533,12 @@ public function ParseOggPageHeader() { $filedata = $this->fread($this->getid3->fread_buffer_size()); $filedataoffset = 0; - while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) { + while (substr($filedata, $filedataoffset++, 4) != 'OggS') { if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) { // should be found before here return false; } - if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) { + if (($filedataoffset + 28) > strlen($filedata)) { if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === '')) { // get some more data, unless eof, in which case fail return false; diff --git a/src/Module/Audio/Rkau.php b/src/Module/Audio/Rkau.php index 17c64b12..cb41f083 100644 --- a/src/Module/Audio/Rkau.php +++ b/src/Module/Audio/Rkau.php @@ -75,7 +75,7 @@ public function Analyze() { $info['audio']['sample_rate'] = $info['rkau']['sample_rate']; $info['playtime_seconds'] = $info['rkau']['source_bytes'] / ($info['rkau']['sample_rate'] * $info['rkau']['channels'] * ($info['rkau']['bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($info['rkau']['compressed_bytes'] * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = Utils::SafeDiv($info['rkau']['compressed_bytes'] * 8, $info['playtime_seconds']); return true; diff --git a/src/Module/Audio/Shorten.php b/src/Module/Audio/Shorten.php index e4cc6d9c..19db81ed 100644 --- a/src/Module/Audio/Shorten.php +++ b/src/Module/Audio/Shorten.php @@ -156,7 +156,7 @@ public function Analyze() { if (substr($output, 20 + $fmt_size, 4) == 'data') { - $info['playtime_seconds'] = Utils::LittleEndian2Int(substr($output, 20 + 4 + $fmt_size, 4)) / $DecodedWAVFORMATEX['raw']['nAvgBytesPerSec']; + $info['playtime_seconds'] = Utils::SafeDiv(Utils::LittleEndian2Int(substr($output, 20 + 4 + $fmt_size, 4)), $DecodedWAVFORMATEX['raw']['nAvgBytesPerSec']); } else { @@ -165,7 +165,7 @@ public function Analyze() { } - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8; + $info['audio']['bitrate'] = Utils::SafeDiv($info['avdataend'] - $info['avdataoffset'], $info['playtime_seconds']) * 8; } else { diff --git a/src/Module/Audio/Tta.php b/src/Module/Audio/Tta.php index f769468f..aa8534c4 100644 --- a/src/Module/Audio/Tta.php +++ b/src/Module/Audio/Tta.php @@ -59,7 +59,7 @@ public function Analyze() { $info['tta']['samples_per_channel'] = Utils::LittleEndian2Int(substr($ttaheader, 12, 4)); $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['samples_per_channel'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['tta']['samples_per_channel'], $info['tta']['sample_rate']); break; case '2': // TTA v2.x @@ -75,7 +75,7 @@ public function Analyze() { $info['tta']['data_length'] = Utils::LittleEndian2Int(substr($ttaheader, 16, 4)); $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['tta']['data_length'], $info['tta']['sample_rate']); break; case '1': // TTA v3.x @@ -91,7 +91,7 @@ public function Analyze() { $info['tta']['crc32_footer'] = substr($ttaheader, 18, 4); $info['tta']['seek_point'] = Utils::LittleEndian2Int(substr($ttaheader, 22, 4)); - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['tta']['data_length'], $info['tta']['sample_rate']); break; default: @@ -103,7 +103,7 @@ public function Analyze() { $info['audio']['bits_per_sample'] = $info['tta']['bits_per_sample']; $info['audio']['sample_rate'] = $info['tta']['sample_rate']; $info['audio']['channels'] = $info['tta']['channels']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['audio']['bitrate'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); return true; } diff --git a/src/Module/Audio/Voc.php b/src/Module/Audio/Voc.php index 447e1472..df6a5c4f 100644 --- a/src/Module/Audio/Voc.php +++ b/src/Module/Audio/Voc.php @@ -140,7 +140,7 @@ public function Analyze() { $thisfile_audio['sample_rate'] = $ThisBlock['sample_rate']; $thisfile_audio['bits_per_sample'] = $ThisBlock['bits_per_sample']; - $thisfile_audio['channels'] = $ThisBlock['channels']; + $thisfile_audio['channels'] = $ThisBlock['channels'] ?: 1; break; default: @@ -164,8 +164,8 @@ public function Analyze() { ksort($thisfile_voc['blocktypes']); if (!empty($thisfile_voc['compressed_bits_per_sample'])) { - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($thisfile_voc['compressed_bits_per_sample'] * $thisfile_audio['channels'] * $thisfile_audio['sample_rate']); - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $info['playtime_seconds'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $thisfile_voc['compressed_bits_per_sample'] * $thisfile_audio['channels'] * $thisfile_audio['sample_rate']); + $thisfile_audio['bitrate'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); } return true; diff --git a/src/Module/Audio/WavPack.php b/src/Module/Audio/WavPack.php index cfc3f409..ac926e1f 100644 --- a/src/Module/Audio/WavPack.php +++ b/src/Module/Audio/WavPack.php @@ -45,7 +45,7 @@ public function Analyze() { if ($this->ftell() >= $info['avdataend']) { break; - } elseif (feof($this->getid3->fp)) { + } elseif ($this->feof()) { break; } elseif ( isset($info['wavpack']['blockheader']['total_samples']) && @@ -160,11 +160,11 @@ public function Analyze() { $info['audio']['lossless'] = !$info['wavpack']['blockheader']['flags']['hybrid']; } - while (!feof($this->getid3->fp) && ($this->ftell() < ($blockheader_offset + $blockheader_size + 8))) { + while (!$this->feof() && ($this->ftell() < ($blockheader_offset + $blockheader_size + 8))) { $metablock = array('offset'=>$this->ftell()); $metablockheader = $this->fread(2); - if (feof($this->getid3->fp)) { + if ($this->feof()) { break; } $metablock['id'] = ord($metablockheader[0]); @@ -245,7 +245,7 @@ public function Analyze() { $metablock['riff']['original_filesize'] = $original_wav_filesize; $info['wavpack']['riff_trailer_size'] = $original_wav_filesize - $metablock['riff']['WAVE']['data'][0]['size'] - $metablock['riff']['header_size']; - $info['playtime_seconds'] = $info['wavpack']['blockheader']['total_samples'] / $info['audio']['sample_rate']; + $info['playtime_seconds'] = Utils::SafeDiv($info['wavpack']['blockheader']['total_samples'], $info['audio']['sample_rate']); // Safe RIFF header in case there's a RIFF footer later $metablockRIFFheader = $metablock['data']; diff --git a/src/Module/AudioVideo/Asf.php b/src/Module/AudioVideo/Asf.php index 67c4ad96..de6f4572 100644 --- a/src/Module/AudioVideo/Asf.php +++ b/src/Module/AudioVideo/Asf.php @@ -195,7 +195,7 @@ public function Analyze() { $info['playtime_seconds'] = ($thisfile_asf_filepropertiesobject['play_duration'] / 10000000) - ($thisfile_asf_filepropertiesobject['preroll'] / 1000); //$info['bitrate'] = $thisfile_asf_filepropertiesobject['max_bitrate']; - $info['bitrate'] = ((isset($thisfile_asf_filepropertiesobject['filesize']) ? $thisfile_asf_filepropertiesobject['filesize'] : $info['filesize']) * 8) / $info['playtime_seconds']; + $info['bitrate'] = Utils::SafeDiv($thisfile_asf_filepropertiesobject['filesize'] * 8, $info['playtime_seconds']); } break; @@ -1066,7 +1066,7 @@ public function Analyze() { break; } - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { + if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { // @phpstan-ignore-line foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { $thisfile_asf_audiomedia_currentstream['bitrate'] = $dataarray['bitrate']; @@ -1152,7 +1152,7 @@ public function Analyze() { $videomediaoffset += 4; $thisfile_asf_videomedia_currentstream['format_data']['codec_data'] = substr($streamdata['type_specific_data'], $videomediaoffset); - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { + if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { // @phpstan-ignore-line foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { $thisfile_asf_videomedia_currentstream['bitrate'] = $dataarray['bitrate']; diff --git a/src/Module/AudioVideo/Ivf.php b/src/Module/AudioVideo/Ivf.php index bf775fff..f139cd8e 100644 --- a/src/Module/AudioVideo/Ivf.php +++ b/src/Module/AudioVideo/Ivf.php @@ -47,7 +47,7 @@ public function Analyze() { $info['ivf']['header']['frame_count'] = Utils::LittleEndian2Int(substr($IVFheader, 24, 4)); //$info['ivf']['header']['reserved'] = substr($IVFheader, 28, 4); - $info['ivf']['header']['frame_rate'] = (float) $info['ivf']['header']['timebase_numerator'] / $info['ivf']['header']['timebase_denominator']; + $info['ivf']['header']['frame_rate'] = (float)Utils::SafeDiv($info['ivf']['header']['timebase_numerator'], $info['ivf']['header']['timebase_denominator']); if ($info['ivf']['header']['version'] > 0) { $this->warning('Expecting IVF header version 0, found version '.$info['ivf']['header']['version'].', results may not be accurate'); @@ -67,7 +67,7 @@ public function Analyze() { $info['ivf']['frame_count']++; } } - if ($info['ivf']['frame_count']) { + if ($info['ivf']['frame_count'] && $info['playtime_seconds']) { $info['playtime_seconds'] = $timestamp / 100000; $info['video']['frame_rate'] = (float) $info['ivf']['frame_count'] / $info['playtime_seconds']; } diff --git a/src/Module/AudioVideo/Matroska.php b/src/Module/AudioVideo/Matroska.php index 944734e8..4b0b345b 100644 --- a/src/Module/AudioVideo/Matroska.php +++ b/src/Module/AudioVideo/Matroska.php @@ -298,12 +298,12 @@ public function Analyze() $track_info['display_x'] = (isset($trackarray['DisplayWidth']) ? $trackarray['DisplayWidth'] : $trackarray['PixelWidth']); $track_info['display_y'] = (isset($trackarray['DisplayHeight']) ? $trackarray['DisplayHeight'] : $trackarray['PixelHeight']); - if (isset($trackarray['PixelCropBottom'])) { $track_info['crop_bottom'] = $trackarray['PixelCropBottom']; } - if (isset($trackarray['PixelCropTop'])) { $track_info['crop_top'] = $trackarray['PixelCropTop']; } - if (isset($trackarray['PixelCropLeft'])) { $track_info['crop_left'] = $trackarray['PixelCropLeft']; } - if (isset($trackarray['PixelCropRight'])) { $track_info['crop_right'] = $trackarray['PixelCropRight']; } - if (isset($trackarray['DefaultDuration'])) { $track_info['frame_rate'] = round(1000000000 / $trackarray['DefaultDuration'], 3); } - if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } + if (isset($trackarray['PixelCropBottom'])) { $track_info['crop_bottom'] = $trackarray['PixelCropBottom']; } + if (isset($trackarray['PixelCropTop'])) { $track_info['crop_top'] = $trackarray['PixelCropTop']; } + if (isset($trackarray['PixelCropLeft'])) { $track_info['crop_left'] = $trackarray['PixelCropLeft']; } + if (isset($trackarray['PixelCropRight'])) { $track_info['crop_right'] = $trackarray['PixelCropRight']; } + if (!empty($trackarray['DefaultDuration'])) { $track_info['frame_rate'] = round(1000000000 / $trackarray['DefaultDuration'], 3); } + if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } switch ($trackarray['CodecID']) { case 'V_MS/VFW/FOURCC': diff --git a/src/Module/AudioVideo/Mpeg.php b/src/Module/AudioVideo/Mpeg.php index 0782aafa..fe24265b 100644 --- a/src/Module/AudioVideo/Mpeg.php +++ b/src/Module/AudioVideo/Mpeg.php @@ -505,9 +505,9 @@ public function Analyze() { $last_GOP_id = max(array_keys($FramesByGOP)); $frames_in_last_GOP = count($FramesByGOP[$last_GOP_id]); $gopdata = &$info['mpeg']['group_of_pictures'][$last_GOP_id]; - $info['playtime_seconds'] = ($gopdata['time_code_hours'] * 3600) + ($gopdata['time_code_minutes'] * 60) + $gopdata['time_code_seconds'] + (($gopdata['time_code_pictures'] + $frames_in_last_GOP + 1) / $info['mpeg']['video']['frame_rate']); + $info['playtime_seconds'] = ($gopdata['time_code_hours'] * 3600) + ($gopdata['time_code_minutes'] * 60) + $gopdata['time_code_seconds'] + Utils::SafeDiv($gopdata['time_code_pictures'] + $frames_in_last_GOP + 1, $info['mpeg']['video']['frame_rate']); if (!isset($info['video']['bitrate'])) { - $overall_bitrate = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['playtime_seconds']; + $overall_bitrate = Utils::SafeDiv($info['avdataend'] - $info['avdataoffset'] * 8, $info['playtime_seconds']); $info['video']['bitrate'] = $overall_bitrate - (isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : 0); } unset($info['mpeg']['group_of_pictures']); @@ -610,14 +610,20 @@ public static function videoFramerateLookup($rawframerate) { * @return float */ public static function videoAspectRatioLookup($rawaspectratio, $mpeg_version=1, $width=0, $height=0) { + // Per http://forum.doom9.org/archive/index.php/t-84400.html + // 0.9157 is commonly accepted to mean 11/12 or .9166, the reciprocal of 12/11 (1.091) and, + // 1.0950 is commonly accepted to mean 11/10 or 1.1, the reciprocal of 10/11 (0.909) $lookup = array( - 1 => array(0, 1, 0.6735, 0.7031, 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, 1.0255, 1.0695, 1.0950, 1.1575, 1.2015, 0), + 1 => array(0, 1, 0.6735, 0.7031, 0.7615, 0.8055, 0.8437, 0.8935, 11/12, 0.9815, 1.0255, 1.0695, 11/10, 1.1575, 1.2015, 0), 2 => array(0, 1, 1.3333, 1.7778, 2.2100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), ); $ratio = (float) (isset($lookup[$mpeg_version][$rawaspectratio]) ? $lookup[$mpeg_version][$rawaspectratio] : 0); if ($mpeg_version == 2 && $ratio != 1 && $width != 0) { // Calculate pixel aspect ratio from MPEG-2 display aspect ratio $ratio = $ratio * $height / $width; + } else if ($mpeg_version == 1 && $ratio !== 0.0) { + // The MPEG-1 tables store the reciprocal of the pixel aspect ratio. + $ratio = 1.0 / $ratio; } return $ratio; } diff --git a/src/Module/AudioVideo/Nsv.php b/src/Module/AudioVideo/Nsv.php index 8bc91960..1ccd4c1a 100644 --- a/src/Module/AudioVideo/Nsv.php +++ b/src/Module/AudioVideo/Nsv.php @@ -215,7 +215,7 @@ public function getNSVfHeaderFilepointer($fileoffset, $getTOCoffsets=false) { } $info['playtime_seconds'] = $info['nsv']['NSVf']['playtime_ms'] / 1000; - $info['bitrate'] = ($info['nsv']['NSVf']['file_size'] * 8) / $info['playtime_seconds']; + $info['bitrate'] = Utils::SafeDiv($info['nsv']['NSVf']['file_size'] * 8, $info['playtime_seconds']); return true; } diff --git a/src/Module/AudioVideo/QuickTime.php b/src/Module/AudioVideo/QuickTime.php index e730dc9e..d4742772 100644 --- a/src/Module/AudioVideo/QuickTime.php +++ b/src/Module/AudioVideo/QuickTime.php @@ -153,7 +153,7 @@ public function Analyze() { } elseif (strlen($lat_deg) == 4) { // [+-]DDMM.M $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval(ltrim(substr($lat_deg, 2, 2), '0').$lat_deg_dec / 60); } elseif (strlen($lat_deg) == 6) { // [+-]DDMMSS.S - $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval(ltrim(substr($lat_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lat_deg, 4, 2), '0').$lat_deg_dec / 3600); + $ISO6709parsed['latitude'] = (($lat_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lat_deg, 0, 2), '0')) + floatval((int) ltrim(substr($lat_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lat_deg, 4, 2), '0').$lat_deg_dec / 3600); } if (strlen($lon_deg) == 3) { // [+-]DDD.D @@ -161,7 +161,7 @@ public function Analyze() { } elseif (strlen($lon_deg) == 5) { // [+-]DDDMM.M $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval(ltrim(substr($lon_deg, 2, 2), '0').$lon_deg_dec / 60); } elseif (strlen($lon_deg) == 7) { // [+-]DDDMMSS.S - $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval(ltrim(substr($lon_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lon_deg, 4, 2), '0').$lon_deg_dec / 3600); + $ISO6709parsed['longitude'] = (($lon_sign == '-') ? -1 : 1) * floatval(ltrim(substr($lon_deg, 0, 2), '0')) + floatval((int) ltrim(substr($lon_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($lon_deg, 4, 2), '0').$lon_deg_dec / 3600); } if (strlen($alt_deg) == 3) { // [+-]DDD.D @@ -169,7 +169,7 @@ public function Analyze() { } elseif (strlen($alt_deg) == 5) { // [+-]DDDMM.M $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval(ltrim(substr($alt_deg, 2, 2), '0').$alt_deg_dec / 60); } elseif (strlen($alt_deg) == 7) { // [+-]DDDMMSS.S - $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval(ltrim(substr($alt_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($alt_deg, 4, 2), '0').$alt_deg_dec / 3600); + $ISO6709parsed['altitude'] = (($alt_sign == '-') ? -1 : 1) * floatval(ltrim(substr($alt_deg, 0, 2), '0')) + floatval((int) ltrim(substr($alt_deg, 2, 2), '0') / 60) + floatval(ltrim(substr($alt_deg, 4, 2), '0').$alt_deg_dec / 3600); } foreach (array('latitude', 'longitude', 'altitude') as $key) { @@ -333,7 +333,7 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset } } elseif (isset($value_array['time_to_sample_table'])) { foreach ($value_array['time_to_sample_table'] as $key2 => $value_array2) { - if (isset($value_array2['sample_count']) && isset($value_array2['sample_duration']) && ($value_array2['sample_duration'] > 0)) { + if (isset($value_array2['sample_count']) && isset($value_array2['sample_duration']) && ($value_array2['sample_duration'] > 0) && !empty($info['quicktime']['time_scale'])) { $framerate = round($info['quicktime']['time_scale'] / $value_array2['sample_duration'], 3); $framecount = $value_array2['sample_count']; } @@ -777,8 +777,8 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset case 'stsd': // Sample Table Sample Description atom - $atom_structure['version'] = Utils::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = Utils::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 + $atom_structure['version'] = Utils::BigEndian2Int(substr($atom_data, 0, 1)); // hardcoded: 0x00 + $atom_structure['flags_raw'] = Utils::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x000000 $atom_structure['number_entries'] = Utils::BigEndian2Int(substr($atom_data, 4, 4)); // see: https://github.com/JamesHeinrich/getID3/issues/111 @@ -806,7 +806,6 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset $stsdEntriesDataOffset += 2; $atom_structure['sample_description_table'][$i]['data'] = substr($atom_data, $stsdEntriesDataOffset, ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2)); $stsdEntriesDataOffset += ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2); - if (substr($atom_structure['sample_description_table'][$i]['data'], 1, 54) == 'application/octet-stream;type=com.parrot.videometadata') { // special handling for apparently-malformed (TextMetaDataSampleEntry?) data for some version of Parrot drones $atom_structure['sample_description_table'][$i]['parrot_frame_metadata']['mime_type'] = substr($atom_structure['sample_description_table'][$i]['data'], 1, 55); @@ -894,7 +893,8 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset break; case 'mp4a': - default: + $atom_structure['sample_description_table'][$i]['subatoms'] = $this->QuicktimeParseContainerAtom(substr($atom_structure['sample_description_table'][$i]['data'], 20), $baseoffset + $stsdEntriesDataOffset - 20 - 16, $atomHierarchy, $ParseAllPossibleAtoms); + $info['quicktime']['audio']['codec'] = $this->QuicktimeAudioCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); $info['quicktime']['audio']['sample_rate'] = $atom_structure['sample_description_table'][$i]['audio_sample_rate']; $info['quicktime']['audio']['channels'] = $atom_structure['sample_description_table'][$i]['audio_channels']; @@ -920,6 +920,9 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset break; } break; + + default: + break; } break; @@ -1665,7 +1668,7 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset ); $atom_structure['data'] = $atom_data; $atom_structure['image_mime'] = 'image/jpeg'; - $atom_structure['description'] = isset($descriptions[$atomname]) ? $descriptions[$atomname] : 'Nikon preview image'; + $atom_structure['description'] = $descriptions[$atomname]; $info['quicktime']['comments']['picture'][] = array( 'image_mime' => $atom_structure['image_mime'], 'data' => $atom_data, @@ -1681,7 +1684,7 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset 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) { + if (ord($atom_data[$i]) <= 0x1F) { $makerNoteVersion .= ' '.ord($atom_data[$i]); } else { $makerNoteVersion .= $atom_data[$i]; @@ -2099,6 +2102,97 @@ public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset break; + case 'esds': // Elementary Stream DeScriptor + // https://github.com/JamesHeinrich/getID3/issues/414 + // https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/formats/mp4/es_descriptor.cc + // https://chromium.googlesource.com/chromium/src/media/+/refs/heads/main/formats/mp4/es_descriptor.h + $atom_structure['version'] = Utils::BigEndian2Int(substr($atom_data, 0, 1)); // hardcoded: 0x00 + $atom_structure['flags_raw'] = Utils::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x000000 + $esds_offset = 4; + + $atom_structure['ES_DescrTag'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + if ($atom_structure['ES_DescrTag'] != 0x03) { + $this->warning('expecting esds.ES_DescrTag = 0x03, found 0x'.Utils::PrintHexBytes($atom_structure['ES_DescrTag']).'), at offset '.$atom_structure['offset']); + break; + } + $atom_structure['ES_DescrSize'] = $this->quicktime_read_mp4_descr_length($atom_data, $esds_offset); + + $atom_structure['ES_ID'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 2)); + $esds_offset += 2; + $atom_structure['ES_flagsraw'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + $atom_structure['ES_flags']['stream_dependency'] = (bool) ($atom_structure['ES_flagsraw'] & 0x80); + $atom_structure['ES_flags']['url_flag'] = (bool) ($atom_structure['ES_flagsraw'] & 0x40); + $atom_structure['ES_flags']['ocr_stream'] = (bool) ($atom_structure['ES_flagsraw'] & 0x20); + $atom_structure['ES_stream_priority'] = ($atom_structure['ES_flagsraw'] & 0x1F); + if ($atom_structure['ES_flags']['url_flag']) { + $this->warning('Unsupported esds.url_flag enabled at offset '.$atom_structure['offset']); + break; + } + if ($atom_structure['ES_flags']['stream_dependency']) { + $atom_structure['ES_dependsOn_ES_ID'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 2)); + $esds_offset += 2; + } + if ($atom_structure['ES_flags']['ocr_stream']) { + $atom_structure['ES_OCR_ES_Id'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 2)); + $esds_offset += 2; + } + + $atom_structure['ES_DecoderConfigDescrTag'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + if ($atom_structure['ES_DecoderConfigDescrTag'] != 0x04) { + $this->warning('expecting esds.ES_DecoderConfigDescrTag = 0x04, found 0x'.Utils::PrintHexBytes($atom_structure['ES_DecoderConfigDescrTag']).'), at offset '.$atom_structure['offset']); + break; + } + $atom_structure['ES_DecoderConfigDescrTagSize'] = $this->quicktime_read_mp4_descr_length($atom_data, $esds_offset); + + $atom_structure['ES_objectTypeIndication'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + // https://stackoverflow.com/questions/3987850 + // 0x40 = "Audio ISO/IEC 14496-3" = MPEG-4 Audio + // 0x67 = "Audio ISO/IEC 13818-7 LowComplexity Profile" = MPEG-2 AAC LC + // 0x69 = "Audio ISO/IEC 13818-3" = MPEG-2 Backward Compatible Audio (MPEG-2 Layers 1, 2, and 3) + // 0x6B = "Audio ISO/IEC 11172-3" = MPEG-1 Audio (MPEG-1 Layers 1, 2, and 3) + + $streamTypePlusFlags = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + $atom_structure['ES_streamType'] = ($streamTypePlusFlags & 0xFC) >> 2; + $atom_structure['ES_upStream'] = (bool) ($streamTypePlusFlags & 0x02) >> 1; + $atom_structure['ES_bufferSizeDB'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 3)); + $esds_offset += 3; + $atom_structure['ES_maxBitrate'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 4)); + $esds_offset += 4; + $atom_structure['ES_avgBitrate'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 4)); + $esds_offset += 4; + if ($atom_structure['ES_avgBitrate']) { + $info['quicktime']['audio']['bitrate'] = $atom_structure['ES_avgBitrate']; + $info['audio']['bitrate'] = $atom_structure['ES_avgBitrate']; + } + + $atom_structure['ES_DecSpecificInfoTag'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + if ($atom_structure['ES_DecSpecificInfoTag'] != 0x05) { + $this->warning('expecting esds.ES_DecSpecificInfoTag = 0x05, found 0x'.Utils::PrintHexBytes($atom_structure['ES_DecSpecificInfoTag']).'), at offset '.$atom_structure['offset']); + break; + } + $atom_structure['ES_DecSpecificInfoTagSize'] = $this->quicktime_read_mp4_descr_length($atom_data, $esds_offset); + + $atom_structure['ES_DecSpecificInfo'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, $atom_structure['ES_DecSpecificInfoTagSize'])); + $esds_offset += $atom_structure['ES_DecSpecificInfoTagSize']; + + $atom_structure['ES_SLConfigDescrTag'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, 1)); + $esds_offset += 1; + if ($atom_structure['ES_SLConfigDescrTag'] != 0x06) { + $this->warning('expecting esds.ES_SLConfigDescrTag = 0x05, found 0x'.Utils::PrintHexBytes($atom_structure['ES_SLConfigDescrTag']).'), at offset '.$atom_structure['offset']); + break; + } + $atom_structure['ES_SLConfigDescrTagSize'] = $this->quicktime_read_mp4_descr_length($atom_data, $esds_offset); + + $atom_structure['ES_SLConfigDescr'] = Utils::BigEndian2Int(substr($atom_data, $esds_offset, $atom_structure['ES_SLConfigDescrTagSize'])); + $esds_offset += $atom_structure['ES_SLConfigDescrTagSize']; + 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 @@ -2989,6 +3083,7 @@ public function quicktime_time_to_sample_table($info) { return array(); } + /** * @param array $info * diff --git a/src/Module/AudioVideo/Real.php b/src/Module/AudioVideo/Real.php index 39dde105..88f8f447 100644 --- a/src/Module/AudioVideo/Real.php +++ b/src/Module/AudioVideo/Real.php @@ -46,8 +46,13 @@ public function Analyze() { $info['audio']['bits_per_sample'] = $info['real']['old_ra_header']['bits_per_sample']; $info['audio']['channels'] = $info['real']['old_ra_header']['channels']; - $info['playtime_seconds'] = 60 * ($info['real']['old_ra_header']['audio_bytes'] / $info['real']['old_ra_header']['bytes_per_minute']); - $info['audio']['bitrate'] = 8 * ($info['real']['old_ra_header']['audio_bytes'] / $info['playtime_seconds']); + if ($info['real']['old_ra_header']['bytes_per_minute']) { + $info['playtime_seconds'] = 60 * ($info['real']['old_ra_header']['audio_bytes'] / $info['real']['old_ra_header']['bytes_per_minute']); + $info['audio']['bitrate'] = 8 * ($info['real']['old_ra_header']['audio_bytes'] / $info['playtime_seconds']); + } else { + $info['playtime_seconds'] = 0; + $info['audio']['bitrate'] = 0; + } $info['audio']['codec'] = $this->RealAudioCodecFourCClookup($info['real']['old_ra_header']['fourcc'], $info['audio']['bitrate']); foreach ($info['real']['old_ra_header']['comments'] as $key => $valuearray) { diff --git a/src/Module/AudioVideo/Riff.php b/src/Module/AudioVideo/Riff.php index 541b00fb..34c7108f 100644 --- a/src/Module/AudioVideo/Riff.php +++ b/src/Module/AudioVideo/Riff.php @@ -214,7 +214,7 @@ public function Analyze() { $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; if (empty($info['playtime_seconds'])) { // may already be set (e.g. DTS-WAV) - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); + $info['playtime_seconds'] = (float)Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $thisfile_audio['bitrate']); } $thisfile_audio['lossless'] = false; @@ -440,11 +440,11 @@ public function Analyze() { $thisfile_riff_WAVE['iXML'][0]['parsed'] = $parsedXML; if (isset($parsedXML['SPEED']['MASTER_SPEED'])) { @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['MASTER_SPEED']); - $thisfile_riff_WAVE['iXML'][0]['master_speed'] = $numerator / ($denominator ? $denominator : 1000); + $thisfile_riff_WAVE['iXML'][0]['master_speed'] = (int) $numerator / ($denominator ? $denominator : 1000); } if (isset($parsedXML['SPEED']['TIMECODE_RATE'])) { @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['TIMECODE_RATE']); - $thisfile_riff_WAVE['iXML'][0]['timecode_rate'] = $numerator / ($denominator ? $denominator : 1000); + $thisfile_riff_WAVE['iXML'][0]['timecode_rate'] = (int) $numerator / ($denominator ? $denominator : 1000); } if (isset($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO']) && !empty($parsedXML['SPEED']['TIMESTAMP_SAMPLE_RATE']) && !empty($thisfile_riff_WAVE['iXML'][0]['timecode_rate'])) { $samples_since_midnight = floatval(ltrim($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_HI'].$parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO'], '0')); @@ -521,7 +521,7 @@ public function Analyze() { if (!isset($thisfile_audio['bitrate']) && isset($thisfile_riff_audio[$streamindex]['bitrate'])) { $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); + $info['playtime_seconds'] = (float)Utils::SafeDiv((($info['avdataend'] - $info['avdataoffset']) * 8), $thisfile_audio['bitrate']); } if (!empty($info['wavpack'])) { @@ -531,7 +531,7 @@ public function Analyze() { // Reset to the way it was - RIFF parsing will have messed this up $info['avdataend'] = $Original['avdataend']; - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; + $thisfile_audio['bitrate'] = Utils::SafeDiv(($info['avdataend'] - $info['avdataoffset']) * 8, $info['playtime_seconds']); $this->fseek($info['avdataoffset'] - 44); $RIFFdata = $this->fread(44); @@ -632,7 +632,7 @@ public function Analyze() { } } if ($info['avdataend'] > $info['filesize']) { - switch (!empty($thisfile_audio_dataformat) ? $thisfile_audio_dataformat : '') { + switch ($thisfile_audio_dataformat) { case 'wavpack': // WavPack case 'lpac': // LPAC case 'ofr': // OptimFROG @@ -672,7 +672,7 @@ public function Analyze() { $this->warning('Extra null byte at end of MP3 data assumed to be RIFF padding and therefore ignored'); } } - if (isset($thisfile_audio_dataformat) && ($thisfile_audio_dataformat == 'ac3')) { + if ($thisfile_audio_dataformat == 'ac3') { unset($thisfile_audio['bits_per_sample']); if (!empty($info['ac3']['bitrate']) && ($info['ac3']['bitrate'] != $thisfile_audio['bitrate'])) { $thisfile_audio['bitrate'] = $info['ac3']['bitrate']; @@ -781,15 +781,15 @@ public function Analyze() { /** @var array $thisfile_riff_video_current */ $thisfile_riff_video_current = &$thisfile_riff_video[$streamindex]; - if ($thisfile_riff_raw_avih['dwWidth'] > 0) { + if ($thisfile_riff_raw_avih['dwWidth'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['frame_width'] = $thisfile_riff_raw_avih['dwWidth']; $thisfile_video['resolution_x'] = $thisfile_riff_video_current['frame_width']; } - if ($thisfile_riff_raw_avih['dwHeight'] > 0) { + if ($thisfile_riff_raw_avih['dwHeight'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['frame_height'] = $thisfile_riff_raw_avih['dwHeight']; $thisfile_video['resolution_y'] = $thisfile_riff_video_current['frame_height']; } - if ($thisfile_riff_raw_avih['dwTotalFrames'] > 0) { + if ($thisfile_riff_raw_avih['dwTotalFrames'] > 0) { // @phpstan-ignore-line $thisfile_riff_video_current['total_frames'] = $thisfile_riff_raw_avih['dwTotalFrames']; $thisfile_video['total_frames'] = $thisfile_riff_video_current['total_frames']; } @@ -1908,7 +1908,7 @@ public function ParseRIFF($startoffset, $maxoffset) { if (isset($RIFFchunk[$chunkname][$thisindex]) && empty($RIFFchunk[$chunkname][$thisindex])) { unset($RIFFchunk[$chunkname][$thisindex]); } - if (isset($RIFFchunk[$chunkname]) && empty($RIFFchunk[$chunkname])) { + if (count($RIFFchunk[$chunkname]) === 0) { unset($RIFFchunk[$chunkname]); } $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['data'] = $this->fread($chunksize); @@ -2029,7 +2029,7 @@ public static function parseComments(&$RIFFinfoArray, &$CommentsTargetArray) { foreach ($RIFFinfoKeyLookup as $key => $value) { if (isset($RIFFinfoArray[$key])) { foreach ($RIFFinfoArray[$key] as $commentid => $commentdata) { - if (trim($commentdata['data']) != '') { + if (!empty($commentdata['data']) && trim($commentdata['data']) != '') { if (isset($CommentsTargetArray[$value])) { $CommentsTargetArray[$value][] = trim($commentdata['data']); } else { diff --git a/src/Module/Graphic/Bmp.php b/src/Module/Graphic/Bmp.php index 38e4c26b..28a1524e 100644 --- a/src/Module/Graphic/Bmp.php +++ b/src/Module/Graphic/Bmp.php @@ -336,307 +336,311 @@ public function Analyze() { } if ($this->ExtractData) { - $this->fseek($thisfile_bmp_header_raw['data_offset']); - $RowByteLength = ceil(($thisfile_bmp_header_raw['width'] * ($thisfile_bmp_header_raw['bits_per_pixel'] / 8)) / 4) * 4; // round up to nearest DWORD boundry - $BMPpixelData = $this->fread($thisfile_bmp_header_raw['height'] * $RowByteLength); - $pixeldataoffset = 0; - $thisfile_bmp_header_raw['compression'] = (isset($thisfile_bmp_header_raw['compression']) ? $thisfile_bmp_header_raw['compression'] : ''); - switch ($thisfile_bmp_header_raw['compression']) { - - case 0: // BI_RGB - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 1: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); - for ($i = 7; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x01 << $i)) >> $i; - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; + if (!$thisfile_bmp_header_raw['width'] || !$thisfile_bmp_header_raw['height']) { + $thisfile_bmp['data'] = array(); + } else { + $this->fseek($thisfile_bmp_header_raw['data_offset']); + $RowByteLength = ceil(($thisfile_bmp_header_raw['width'] * ($thisfile_bmp_header_raw['bits_per_pixel'] / 8)) / 4) * 4; // round up to nearest DWORD boundry + $BMPpixelData = $this->fread($thisfile_bmp_header_raw['height'] * $RowByteLength); + $pixeldataoffset = 0; + $thisfile_bmp_header_raw['compression'] = (isset($thisfile_bmp_header_raw['compression']) ? $thisfile_bmp_header_raw['compression'] : ''); + switch ($thisfile_bmp_header_raw['compression']) { + + case 0: // BI_RGB + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 1: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { + $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); + for ($i = 7; $i >= 0; $i--) { + $paletteindex = ($paletteindexbyte & (0x01 << $i)) >> $i; + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $col++; + } + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; + break; + + case 4: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { + $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); + for ($i = 1; $i >= 0; $i--) { + $paletteindex = ($paletteindexbyte & (0x0F << (4 * $i))) >> (4 * $i); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $col++; + } + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; + } } - } - break; - - case 4: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData[$pixeldataoffset++]); - for ($i = 1; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x0F << (4 * $i))) >> (4 * $i); + break; + + case 8: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $paletteindex = ord($BMPpixelData[$pixeldataoffset++]); $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 8: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $paletteindex = ord($BMPpixelData[$pixeldataoffset++]); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 24: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); - $pixeldataoffset += 3; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 32: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+3]) << 24) | (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); - $pixeldataoffset += 4; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; + break; - case 16: - // ? - break; - - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; - - - case 1: // BI_RLE8 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 8: - $pixelcounter = 0; - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { - - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; - - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; - - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; - - default: - // In absolute mode, the first byte is zero and the second byte is a - // value in the range 03H through FFH. The second byte represents the - // number of bytes that follow, each of which contains the color index - // of a single pixel. Each run must be aligned on a word boundary. - for ($i = 0; $i < $secondbyte; $i++) { - $paletteindex = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } - break; + case 24: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); + $pixeldataoffset += 3; } - - } else { - - // encoded mode - the first byte specifies the number of consecutive pixels - // to be drawn using the color index contained in the second byte. - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$secondbyte]; - $pixelcounter++; + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } - } - } - break; + break; - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; - - - - case 2: // BI_RLE4 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 4: - $pixelcounter = 0; - $paletteindexes = array(); - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { - - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; - - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; - - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; - - default: - // In absolute mode, the first byte is zero. The second byte contains the number - // of color indexes that follow. Subsequent bytes contain color indexes in their - // high- and low-order 4 bits, one color index for each pixel. In absolute mode, - // each run must be aligned on a word boundary. - $paletteindexes = array(); - for ($i = 0; $i < ceil($secondbyte / 2); $i++) { - $paletteindexbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $paletteindexes[] = ($paletteindexbyte & 0xF0) >> 4; - $paletteindexes[] = ($paletteindexbyte & 0x0F); - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } - - foreach ($paletteindexes as $paletteindex) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - break; + case 32: + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData[$pixeldataoffset+3]) << 24) | (ord($BMPpixelData[$pixeldataoffset+2]) << 16) | (ord($BMPpixelData[$pixeldataoffset+1]) << 8) | ord($BMPpixelData[$pixeldataoffset]); + $pixeldataoffset += 4; } - - } else { - - // encoded mode - the first byte of the pair contains the number of pixels to be - // drawn using the color indexes in the second byte. The second byte contains two - // color indexes, one in its high-order 4 bits and one in its low-order 4 bits. - // The first of the pixels is drawn using the color specified by the high-order - // 4 bits, the second is drawn using the color in the low-order 4 bits, the third - // is drawn using the color in the high-order 4 bits, and so on, until all the - // pixels specified by the first byte have been drawn. - $paletteindexes[0] = ($secondbyte & 0xF0) >> 4; - $paletteindexes[1] = ($secondbyte & 0x0F); - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindexes[($i % 2)]]; - $pixelcounter++; + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; } + } + break; + + case 16: + // ? + break; + + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; + + + case 1: // BI_RLE8 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 8: + $pixelcounter = 0; + while ($pixeldataoffset < strlen($BMPpixelData)) { + $firstbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $secondbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + if ($firstbyte == 0) { + + // escaped/absolute mode - the first byte of the pair can be set to zero to + // indicate an escape character that denotes the end of a line, the end of + // a bitmap, or a delta, depending on the value of the second byte. + switch ($secondbyte) { + case 0: + // end of line + // no need for special processing, just ignore + break; + + case 1: + // end of bitmap + $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case + break; + + case 2: + // delta - The 2 bytes following the escape contain unsigned values + // indicating the horizontal and vertical offsets of the next pixel + // from the current position. + $colincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $rowincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; + $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; + $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; + break; + + default: + // In absolute mode, the first byte is zero and the second byte is a + // value in the range 03H through FFH. The second byte represents the + // number of bytes that follow, each of which contains the color index + // of a single pixel. Each run must be aligned on a word boundary. + for ($i = 0; $i < $secondbyte; $i++) { + $paletteindex = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $pixelcounter++; + } + while (($pixeldataoffset % 2) != 0) { + // Each run must be aligned on a word boundary. + $pixeldataoffset++; + } + break; + } + + } else { + + // encoded mode - the first byte specifies the number of consecutive pixels + // to be drawn using the color index contained in the second byte. + for ($i = 0; $i < $firstbyte; $i++) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$secondbyte]; + $pixelcounter++; + } + } } - } - break; + break; + + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; + + + + case 2: // BI_RLE4 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 4: + $pixelcounter = 0; + $paletteindexes = array(); + while ($pixeldataoffset < strlen($BMPpixelData)) { + $firstbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $secondbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + if ($firstbyte == 0) { + + // escaped/absolute mode - the first byte of the pair can be set to zero to + // indicate an escape character that denotes the end of a line, the end of + // a bitmap, or a delta, depending on the value of the second byte. + switch ($secondbyte) { + case 0: + // end of line + // no need for special processing, just ignore + break; + + case 1: + // end of bitmap + $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case + break; + + case 2: + // delta - The 2 bytes following the escape contain unsigned values + // indicating the horizontal and vertical offsets of the next pixel + // from the current position. + $colincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $rowincrement = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; + $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; + $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; + break; + + default: + // In absolute mode, the first byte is zero. The second byte contains the number + // of color indexes that follow. Subsequent bytes contain color indexes in their + // high- and low-order 4 bits, one color index for each pixel. In absolute mode, + // each run must be aligned on a word boundary. + $paletteindexes = array(); + for ($i = 0; $i < ceil($secondbyte / 2); $i++) { + $paletteindexbyte = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); + $paletteindexes[] = ($paletteindexbyte & 0xF0) >> 4; + $paletteindexes[] = ($paletteindexbyte & 0x0F); + } + while (($pixeldataoffset % 2) != 0) { + // Each run must be aligned on a word boundary. + $pixeldataoffset++; + } + + foreach ($paletteindexes as $paletteindex) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; + $pixelcounter++; + } + break; + } + + } else { + + // encoded mode - the first byte of the pair contains the number of pixels to be + // drawn using the color indexes in the second byte. The second byte contains two + // color indexes, one in its high-order 4 bits and one in its low-order 4 bits. + // The first of the pixels is drawn using the color specified by the high-order + // 4 bits, the second is drawn using the color in the low-order 4 bits, the third + // is drawn using the color in the high-order 4 bits, and so on, until all the + // pixels specified by the first byte have been drawn. + $paletteindexes[0] = ($secondbyte & 0xF0) >> 4; + $paletteindexes[1] = ($secondbyte & 0x0F); + for ($i = 0; $i < $firstbyte; $i++) { + $col = $pixelcounter % $thisfile_bmp_header_raw['width']; + $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); + $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindexes[($i % 2)]]; + $pixelcounter++; + } - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; - - - case 3: // BI_BITFIELDS - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 16: - case 32: - $redshift = 0; - $greenshift = 0; - $blueshift = 0; - while ((($thisfile_bmp_header_raw['red_mask'] >> $redshift) & 0x01) == 0) { - $redshift++; - } - while ((($thisfile_bmp_header_raw['green_mask'] >> $greenshift) & 0x01) == 0) { - $greenshift++; - } - while ((($thisfile_bmp_header_raw['blue_mask'] >> $blueshift) & 0x01) == 0) { - $blueshift++; - } - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $pixelvalue = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset, $thisfile_bmp_header_raw['bits_per_pixel'] / 8)); - $pixeldataoffset += $thisfile_bmp_header_raw['bits_per_pixel'] / 8; - - $red = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['red_mask']) >> $redshift) / ($thisfile_bmp_header_raw['red_mask'] >> $redshift)) * 255)); - $green = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['green_mask']) >> $greenshift) / ($thisfile_bmp_header_raw['green_mask'] >> $greenshift)) * 255)); - $blue = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['blue_mask']) >> $blueshift) / ($thisfile_bmp_header_raw['blue_mask'] >> $blueshift)) * 255)); - $thisfile_bmp['data'][$row][$col] = (($red << 16) | ($green << 8) | ($blue)); + } + } + break; + + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; + + + case 3: // BI_BITFIELDS + switch ($thisfile_bmp_header_raw['bits_per_pixel']) { + case 16: + case 32: + $redshift = 0; + $greenshift = 0; + $blueshift = 0; + while ((($thisfile_bmp_header_raw['red_mask'] >> $redshift) & 0x01) == 0) { + $redshift++; + } + while ((($thisfile_bmp_header_raw['green_mask'] >> $greenshift) & 0x01) == 0) { + $greenshift++; + } + while ((($thisfile_bmp_header_raw['blue_mask'] >> $blueshift) & 0x01) == 0) { + $blueshift++; } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; + for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { + for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { + $pixelvalue = Utils::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset, $thisfile_bmp_header_raw['bits_per_pixel'] / 8)); + $pixeldataoffset += $thisfile_bmp_header_raw['bits_per_pixel'] / 8; + + $red = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['red_mask']) >> $redshift) / ($thisfile_bmp_header_raw['red_mask'] >> $redshift)) * 255)); + $green = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['green_mask']) >> $greenshift) / ($thisfile_bmp_header_raw['green_mask'] >> $greenshift)) * 255)); + $blue = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['blue_mask']) >> $blueshift) / ($thisfile_bmp_header_raw['blue_mask'] >> $blueshift)) * 255)); + $thisfile_bmp['data'][$row][$col] = (($red << 16) | ($green << 8) | ($blue)); + } + while (($pixeldataoffset % 4) != 0) { + // lines are padded to nearest DWORD + $pixeldataoffset++; + } } - } - break; + break; - default: - $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); - break; - } - break; + default: + $this->error('Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'); + break; + } + break; - default: // unhandled compression type - $this->error('Unknown/unhandled compression type value ('.$thisfile_bmp_header_raw['compression'].') - cannot decompress pixel data'); - break; + default: // unhandled compression type + $this->error('Unknown/unhandled compression type value ('.$thisfile_bmp_header_raw['compression'].') - cannot decompress pixel data'); + break; + } } } diff --git a/src/Module/Graphic/Jpg.php b/src/Module/Graphic/Jpg.php index b18cb29d..ec0619c6 100644 --- a/src/Module/Graphic/Jpg.php +++ b/src/Module/Graphic/Jpg.php @@ -184,7 +184,7 @@ public function Analyze() { * @return mixed */ public function CastAsAppropriate($value) { - if (is_array($value)) { + if (is_array($value) || is_null($value)) { return $value; } elseif (preg_match('#^[0-9]+/[0-9]+$#', $value)) { return Utils::DecimalizeFraction($value); diff --git a/src/Module/Handler.php b/src/Module/Handler.php index d95a94fc..cf10fc1f 100644 --- a/src/Module/Handler.php +++ b/src/Module/Handler.php @@ -109,6 +109,8 @@ public function setStringMode($string) { } /** + * @phpstan-impure + * * @return int|bool */ protected function ftell() { @@ -121,6 +123,8 @@ protected function ftell() { /** * @param int $bytes * + * @phpstan-impure + * * @return string|false * * @throws Exception @@ -166,6 +170,8 @@ protected function fread($bytes) { * @param int $bytes * @param int $whence * + * @phpstan-impure + * * @return int * * @throws Exception @@ -207,6 +213,8 @@ protected function fseek($bytes, $whence=SEEK_SET) { } /** + * @phpstan-impure + * * @return string|false * * @throws Exception @@ -262,6 +270,8 @@ protected function fgets() { } /** + * @phpstan-impure + * * @return bool */ protected function feof() { diff --git a/src/Module/Tag/ApeTag.php b/src/Module/Tag/ApeTag.php index 337390dd..7c339a1f 100644 --- a/src/Module/Tag/ApeTag.php +++ b/src/Module/Tag/ApeTag.php @@ -267,7 +267,7 @@ public function Analyze() { case 'cover art (publisher logo)': case 'cover art (recording)': case 'cover art (studio)': - // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html + // list of possible cover arts from https://github.com/mono/taglib-sharp/blob/taglib-sharp-2.0.3.2/src/TagLib/Ape/Tag.cs if (is_array($thisfile_ape_items_current['data'])) { $this->warning('APEtag "'.$item_key.'" should be flagged as Binary data, but was incorrectly flagged as UTF-8'); $thisfile_ape_items_current['data'] = implode("\x00", $thisfile_ape_items_current['data']); @@ -332,7 +332,7 @@ public function Analyze() { $info['ape']['comments']['picture'][] = $comments_picture_data; unset($comments_picture_data); } - } while (false); + } while (false); // @phpstan-ignore-line break; default: diff --git a/src/Module/Tag/ID3v1.php b/src/Module/Tag/ID3v1.php index c370e7cf..9b9c6aea 100644 --- a/src/Module/Tag/ID3v1.php +++ b/src/Module/Tag/ID3v1.php @@ -66,7 +66,7 @@ public function Analyze() { if (!empty($ParsedID3v1['genre'])) { unset($ParsedID3v1['genreid']); } - if (isset($ParsedID3v1['genre']) && (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown'))) { + if (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown')) { unset($ParsedID3v1['genre']); } diff --git a/src/Module/Tag/ID3v2.php b/src/Module/Tag/ID3v2.php index a9300347..36306ec1 100644 --- a/src/Module/Tag/ID3v2.php +++ b/src/Module/Tag/ID3v2.php @@ -1493,7 +1493,7 @@ public function ParseID3v2Frame(&$parsedFrame) { unset($comments_picture_data); } } - } while (false); + } while (false); // @phpstan-ignore-line } } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GEOB')) || // 4.15 GEOB General encapsulated object @@ -3752,18 +3752,12 @@ public static function IsValidID3v2FrameName($framename, $id3v2majorversion) { * @return bool */ public static function IsANumber($numberstring, $allowdecimal=false, $allownegative=false) { - for ($i = 0; $i < strlen($numberstring); $i++) { - if ((chr($numberstring[$i]) < chr('0')) || (chr($numberstring[$i]) > chr('9'))) { - if (($numberstring[$i] == '.') && $allowdecimal) { - // allowed - } elseif (($numberstring[$i] == '-') && $allownegative && ($i == 0)) { - // allowed - } else { - return false; - } - } - } - return true; + $pattern = '#^'; + $pattern .= ($allownegative ? '\\-?' : ''); + $pattern .= '[0-9]+'; + $pattern .= ($allowdecimal ? '(\\.[0-9]+)?' : ''); + $pattern .= '$#'; + return preg_match($pattern, $numberstring); } /** @@ -3772,10 +3766,7 @@ public static function IsANumber($numberstring, $allowdecimal=false, $allownegat * @return bool */ public static function IsValidDateStampString($datestamp) { - if (strlen($datestamp) != 8) { - return false; - } - if (!self::IsANumber($datestamp, false)) { + if (!preg_match('#^[12][0-9]{3}[01][0-9][0123][0-9]$#', $datestamp)) { return false; } $year = substr($datestamp, 0, 4); diff --git a/src/Module/Tag/Xmp.php b/src/Module/Tag/Xmp.php index 03c8f0dd..acb58299 100644 --- a/src/Module/Tag/Xmp.php +++ b/src/Module/Tag/Xmp.php @@ -34,6 +34,77 @@ *************************************************************************************************/ class Xmp { + /** + * The names of the JPEG segment markers, indexed by their marker number + */ + private static $jpeg_segments_name = array( + 0x01 => 'TEM', + 0x02 => 'RES', + 0xC0 => 'SOF0', + 0xC1 => 'SOF1', + 0xC2 => 'SOF2', + 0xC3 => 'SOF4', + 0xC4 => 'DHT', + 0xC5 => 'SOF5', + 0xC6 => 'SOF6', + 0xC7 => 'SOF7', + 0xC8 => 'JPG', + 0xC9 => 'SOF9', + 0xCA => 'SOF10', + 0xCB => 'SOF11', + 0xCC => 'DAC', + 0xCD => 'SOF13', + 0xCE => 'SOF14', + 0xCF => 'SOF15', + 0xD0 => 'RST0', + 0xD1 => 'RST1', + 0xD2 => 'RST2', + 0xD3 => 'RST3', + 0xD4 => 'RST4', + 0xD5 => 'RST5', + 0xD6 => 'RST6', + 0xD7 => 'RST7', + 0xD8 => 'SOI', + 0xD9 => 'EOI', + 0xDA => 'SOS', + 0xDB => 'DQT', + 0xDC => 'DNL', + 0xDD => 'DRI', + 0xDE => 'DHP', + 0xDF => 'EXP', + 0xE0 => 'APP0', + 0xE1 => 'APP1', + 0xE2 => 'APP2', + 0xE3 => 'APP3', + 0xE4 => 'APP4', + 0xE5 => 'APP5', + 0xE6 => 'APP6', + 0xE7 => 'APP7', + 0xE8 => 'APP8', + 0xE9 => 'APP9', + 0xEA => 'APP10', + 0xEB => 'APP11', + 0xEC => 'APP12', + 0xED => 'APP13', + 0xEE => 'APP14', + 0xEF => 'APP15', + 0xF0 => 'JPG0', + 0xF1 => 'JPG1', + 0xF2 => 'JPG2', + 0xF3 => 'JPG3', + 0xF4 => 'JPG4', + 0xF5 => 'JPG5', + 0xF6 => 'JPG6', + 0xF7 => 'JPG7', + 0xF8 => 'JPG8', + 0xF9 => 'JPG9', + 0xFA => 'JPG10', + 0xFB => 'JPG11', + 0xFC => 'JPG12', + 0xFD => 'JPG13', + 0xFE => 'COM', + ); + /** * @var string * The name of the image file that contains the XMP fields to extract and modify. @@ -147,12 +218,21 @@ public function _get_jpeg_header_data($filename) $segdatastart = ftell($filehnd); // Read the segment data with length indicated by the previously read size - $segdata = fread($filehnd, $decodedsize['size'] - 2); + // fread will complain about trying to read zero bytes: "fread(): Argument #2 ($length) must be greater than 0" -- https://github.com/JamesHeinrich/getID3/issues/418 + if ($decodedsize['size'] > 2) { + $segdata = fread($filehnd, $decodedsize['size'] - 2); + } elseif ($decodedsize['size'] == 2) { + $segdata = ''; + } else { + // invalid length + fclose($filehnd); + return false; + } // Store the segment information in the output array $headerdata[] = array( 'SegType' => ord($data[1]), - 'SegName' => $GLOBALS['JPEG_Segment_Names'][ord($data[1])], + 'SegName' => self::$jpeg_segments_name[ord($data[1])], 'SegDataStart' => $segdatastart, 'SegData' => $segdata, ); @@ -702,77 +782,4 @@ public function __construct($sFilename) 'exif:Rows', 'exif:Settings', ); -*/ - -/** -* Global Variable: JPEG_Segment_Names -* -* The names of the JPEG segment markers, indexed by their marker number -*/ -$GLOBALS['JPEG_Segment_Names'] = array( - 0x01 => 'TEM', - 0x02 => 'RES', - 0xC0 => 'SOF0', - 0xC1 => 'SOF1', - 0xC2 => 'SOF2', - 0xC3 => 'SOF4', - 0xC4 => 'DHT', - 0xC5 => 'SOF5', - 0xC6 => 'SOF6', - 0xC7 => 'SOF7', - 0xC8 => 'JPG', - 0xC9 => 'SOF9', - 0xCA => 'SOF10', - 0xCB => 'SOF11', - 0xCC => 'DAC', - 0xCD => 'SOF13', - 0xCE => 'SOF14', - 0xCF => 'SOF15', - 0xD0 => 'RST0', - 0xD1 => 'RST1', - 0xD2 => 'RST2', - 0xD3 => 'RST3', - 0xD4 => 'RST4', - 0xD5 => 'RST5', - 0xD6 => 'RST6', - 0xD7 => 'RST7', - 0xD8 => 'SOI', - 0xD9 => 'EOI', - 0xDA => 'SOS', - 0xDB => 'DQT', - 0xDC => 'DNL', - 0xDD => 'DRI', - 0xDE => 'DHP', - 0xDF => 'EXP', - 0xE0 => 'APP0', - 0xE1 => 'APP1', - 0xE2 => 'APP2', - 0xE3 => 'APP3', - 0xE4 => 'APP4', - 0xE5 => 'APP5', - 0xE6 => 'APP6', - 0xE7 => 'APP7', - 0xE8 => 'APP8', - 0xE9 => 'APP9', - 0xEA => 'APP10', - 0xEB => 'APP11', - 0xEC => 'APP12', - 0xED => 'APP13', - 0xEE => 'APP14', - 0xEF => 'APP15', - 0xF0 => 'JPG0', - 0xF1 => 'JPG1', - 0xF2 => 'JPG2', - 0xF3 => 'JPG3', - 0xF4 => 'JPG4', - 0xF5 => 'JPG5', - 0xF6 => 'JPG6', - 0xF7 => 'JPG7', - 0xF8 => 'JPG8', - 0xF9 => 'JPG9', - 0xFA => 'JPG10', - 0xFB => 'JPG11', - 0xFC => 'JPG12', - 0xFD => 'JPG13', - 0xFE => 'COM', -); +*/ \ No newline at end of file diff --git a/src/Utils.php b/src/Utils.php index 3bee5b86..8f2dae44 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -277,6 +277,18 @@ public static function intValueSupported($num) return false; } + /** + * Perform a division, guarding against division by zero + * + * @param float|int $numerator + * @param float|int $denominator + * @param float|int $fallback + * @return float|int + */ + public static function SafeDiv($numerator, $denominator, $fallback = 0) { + return $denominator ? $numerator / $denominator : $fallback; + } + /** * @param string $fraction * @@ -284,7 +296,7 @@ public static function intValueSupported($num) */ public static function DecimalizeFraction($fraction) { list($numerator, $denominator) = explode('/', $fraction); - return $numerator / ($denominator ? $denominator : 1); + return (int) $numerator / ($denominator ? $denominator : 1); } /** @@ -1021,10 +1033,6 @@ public static function iconv_fallback_int_utf8($charval) { * @return string */ public static function iconv_fallback_iso88591_utf8($string, $bom=false) { - if (function_exists('utf8_encode')) { - return utf8_encode($string); - } - // utf8_encode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) $newcharstring = ''; if ($bom) { $newcharstring .= "\xEF\xBB\xBF"; @@ -1093,10 +1101,6 @@ public static function iconv_fallback_iso88591_utf16($string) { * @return string */ public static function iconv_fallback_utf8_iso88591($string) { - if (function_exists('utf8_decode')) { - return utf8_decode($string); - } - // utf8_decode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) $newcharstring = ''; $offset = 0; $stringlength = strlen($string); diff --git a/src/Write/ID3v1.php b/src/Write/ID3v1.php index 8db6a52c..f9a399c8 100644 --- a/src/Write/ID3v1.php +++ b/src/Write/ID3v1.php @@ -77,7 +77,8 @@ public function WriteID3v1() { (isset($this->tag_data['year'] ) ? $this->tag_data['year'] : ''), (isset($this->tag_data['genreid'] ) ? $this->tag_data['genreid'] : ''), (isset($this->tag_data['comment'] ) ? $this->tag_data['comment'] : ''), - (isset($this->tag_data['track_number']) ? $this->tag_data['track_number'] : '')); + $this->tag_data['track_number'] + ); fwrite($fp_source, $new_id3v1_tag_data, 128); fclose($fp_source); return true; diff --git a/src/Write/ID3v2.php b/src/Write/ID3v2.php index 03cfdf88..7bf46329 100644 --- a/src/Write/ID3v2.php +++ b/src/Write/ID3v2.php @@ -123,26 +123,12 @@ public function WriteID3v2() { if (file_exists($this->filename) && Utils::isWritable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) { // best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary) - if (file_exists($this->filename)) { - - if (is_readable($this->filename) && Utils::isWritable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; - } - + if (is_readable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) { + rewind($fp); + fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); + fclose($fp); } else { - - if (Utils::isWritable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")'; - } - + $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; } } else { @@ -221,7 +207,7 @@ public function RemoveID3v2() { if ($OldThisFileInfo['avdataoffset'] !== false) { fseek($fp_source, $OldThisFileInfo['avdataoffset']); } - if (Utils::isWritable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) { + if (Utils::isWritable($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) { while ($buffer = fread($fp_source, $this->fread_buffer_size)) { fwrite($fp_temp, $buffer, strlen($buffer)); } @@ -261,7 +247,7 @@ public function RemoveID3v2() { fwrite($fp_temp, $buffer, strlen($buffer)); } fclose($fp_source); - if (Utils::isWritable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { + if (Utils::isWritable($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { rewind($fp_temp); while ($buffer = fread($fp_temp, $this->fread_buffer_size)) { fwrite($fp_source, $buffer, strlen($buffer)); diff --git a/src/WriteTags.php b/src/WriteTags.php index 03d4677d..4c91c56d 100644 --- a/src/WriteTags.php +++ b/src/WriteTags.php @@ -661,7 +661,7 @@ public function FormatDataForID3v2($id3v2_majorversion) { $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0; $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; $ID3v2_tag_data_converted = true; - } while (false); + } while (false); // @phpstan-ignore-line } if (!$ID3v2_tag_data_converted) { $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 1;