Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mediainfo.class.php 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. <?php
  2. class MediaInfo
  3. {
  4. public static function parse($string, $raw = false)
  5. {
  6. $t = new ParseManager($string);
  7. $t->parse();
  8. return $raw ? $t->output_raw() : $t->output();
  9. }
  10. }
  11. class ParseManager
  12. {
  13. protected $lines;
  14. protected $parsed_lines;
  15. protected $index;
  16. protected $parsers;
  17. protected $output;
  18. protected $available_parsers = array(
  19. 'general'=> 'GeneralSectionParser',
  20. 'video'=> 'VideoSectionParser',
  21. 'audio'=> 'AudioSectionParser',
  22. 'text'=> 'TextSectionParser',
  23. );
  24. const GENERIC_PARSER = 'generic_parser';
  25. const MEDIAINFO_START = 'general';
  26. public function __construct($string = '')
  27. {
  28. $this->index = 0;
  29. $this->output = '';
  30. $this->parsed_lines = [];
  31. $this->set_string($string);
  32. $p = new SectionParser();
  33. $this->add_parser($p);
  34. }
  35. protected function set_string($string)
  36. {
  37. $string = trim($string);
  38. $string = static::strip_escaped_tags($string);
  39. $lines = preg_split("/\r\n|\n|\r/", $string);
  40. array_walk($lines, function (&$l) {
  41. $l=trim($l);
  42. });
  43. $this->lines = $lines;
  44. }
  45. protected function add_parser($p, $name = '')
  46. {
  47. $p->set_lines($this->lines, $this->index);
  48. if (!$name) {
  49. $name = static::GENERIC_PARSER;
  50. }
  51. $this->parsers[$name][] = $p;
  52. }
  53. public function parse()
  54. {
  55. // get the next section
  56. while ($this->index < count($this->lines) &&
  57. !($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line())) {
  58. }
  59. $section = SectionParser::section_name($line);
  60. $this->index--; // go back to line we just read
  61. // we can have multiple mediainfo files inside the block, so handle that case here
  62. if ($section == self::MEDIAINFO_START && isset($this->parsers[$section])) {
  63. $this->new_mediainfo();
  64. }
  65. if (isset($this->available_parsers[$section])) {
  66. $parser = new $this->available_parsers[$section];
  67. $this->add_parser($parser, $section);
  68. // parse section using the parser
  69. while ($line = $parser->parse_line()) {
  70. $this->parsed_lines[] = $line;
  71. }
  72. $this->parsed_lines[] = '';
  73. } else {
  74. // skip until the next blank line or until the next general section
  75. while ($line = $this->parsers[static::GENERIC_PARSER][0]->parse_line()) {
  76. $section = SectionParser::section_name($line);
  77. if ($section == self::MEDIAINFO_START) {
  78. $this->index--; // go back to line we just read
  79. break;
  80. }
  81. }
  82. }
  83. // keep iterating until the last line
  84. if ($this->index < count($this->lines)) {
  85. $this->parse();
  86. }
  87. }
  88. public function output($cummulative = true)
  89. {
  90. $string = implode("<br />\n", $this->parsed_lines);
  91. if (!isset($this->parsers['general'])) {
  92. return $string;
  93. }
  94. $midiv_start = '<div class="spoilerContainer hideContainer">
  95. <input type="button" class="spoilerButton" value="Show '.
  96. $this->parsers['general'][0]->filename.
  97. '" /><blockquote class="spoiler hidden">';
  98. $midiv_end = "</blockquote></div>";
  99. $output = '<table class="mediainfo"><tbody><tr><td>';
  100. $output .= $this->parsers['general'][0]->output();
  101. if (isset($this->parsers['video'])) {
  102. $output .= '</td><td>';
  103. $output .= $this->parsers['video'][0]->output();
  104. }
  105. if (isset($this->parsers['audio'])) {
  106. $output .= '</td><td>';
  107. $output .= '<table><caption>Audio</caption><tbody>';
  108. foreach ($this->parsers['audio'] as $index => $ap) {
  109. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>', intval($index+1), $ap->output());
  110. }
  111. $output .= '</tbody></table>';
  112. }
  113. if (isset($this->parsers['text'])) {
  114. // subtitles table will be printed below the previous table
  115. $output .= '<br />';
  116. $output .= '<table><caption>Subtitles</caption><tbody>';
  117. foreach ($this->parsers['text'] as $index => $tp) {
  118. $output .= sprintf('<tr><td>#%d: &nbsp;</td><td>%s</td></tr>', intval($index+1), $tp->output());
  119. }
  120. $output .= '</tbody></table>';
  121. }
  122. $output .= '</td></tr></tbody></table><br />';
  123. $output = $midiv_start . $string . $midiv_end . $output;
  124. if ($cummulative) {
  125. $output = $this->output . $output;
  126. }
  127. return $output;
  128. }
  129. public function output_raw()
  130. {
  131. $output = [];
  132. $sections = ['general', 'video', 'audio', 'text'];
  133. foreach ($sections as $section) {
  134. if (isset($this->parsers[$section])) {
  135. $output[$section] = [];
  136. foreach ($this->parsers[$section] as $index => $parser) {
  137. $output[$section][$index] = $parser->output_raw();
  138. }
  139. }
  140. }
  141. return $output;
  142. }
  143. // strip escaped html tags
  144. // this is not done for security, just to beautify things (html should already be escaped)
  145. public static function strip_escaped_tags($string)
  146. {
  147. // use the php function first
  148. $string = strip_tags($string);
  149. $gt = '&gt;|&#62;|>';
  150. $lt = '&lt;|&#60;|<';
  151. // there is no opening tag, so don't go through the rest of the regexes
  152. if (!preg_match("($lt)", $string)) {
  153. return $string;
  154. }
  155. $tag_match = "/(?:$lt)(?P<tag>(?:(?!$gt).)*)(?:$gt)/ims";
  156. // this should match and remove tags
  157. $string = preg_replace($tag_match, '', $string);
  158. return $string;
  159. }
  160. protected function new_mediainfo()
  161. {
  162. $this->output .= $this->output(false);
  163. $this->parsed_lines = [];
  164. foreach (array_keys($this->parsers) as $key) {
  165. if ($key != static::GENERIC_PARSER) {
  166. unset($this->parsers[$key]);
  167. }
  168. }
  169. }
  170. }
  171. class SectionParser
  172. {
  173. protected $lines;
  174. protected $index;
  175. public function __construct(&$lines = [], &$i = 0)
  176. {
  177. $this->set_lines($lines, $i);
  178. }
  179. public function set_lines(&$lines = [], &$i = 0)
  180. {
  181. $this->lines = &$lines;
  182. $this->index = &$i;
  183. }
  184. // should always return the read line
  185. public function parse_line()
  186. {
  187. if (!isset($this->lines[$this->index])) {
  188. return null;
  189. }
  190. $line = $this->lines[$this->index++];
  191. $pair = static::property_value_pair($line);
  192. $this->handle_cases($pair['property'], $pair['value']);
  193. return $line;
  194. }
  195. public function output()
  196. {
  197. }
  198. protected function handle_cases($property, $value)
  199. {
  200. }
  201. public static function section_name($string)
  202. {
  203. if (!$string) {
  204. return false;
  205. }
  206. $mistart="/^(?:general$|unique id|complete name)/i";
  207. if (preg_match($mistart, $string)) {
  208. return ParseManager::MEDIAINFO_START;
  209. }
  210. $words = explode(' ', $string);
  211. return strtolower($words[0]);
  212. }
  213. public static function property_value_pair($string)
  214. {
  215. $pair = explode(":", $string, 2);
  216. return array('property'=>strtolower(trim($pair[0])),'value'=>trim($pair[1]??''));
  217. }
  218. public static function strip_path($string)
  219. {
  220. // remove filepath
  221. $string = str_replace("\\", "/", $string);
  222. $path_parts = pathinfo($string);
  223. return $path_parts['basename'];
  224. }
  225. public static function parse_size($string)
  226. {
  227. return str_replace(array('pixels', ' '), null, $string);
  228. }
  229. protected static function table_head($caption)
  230. {
  231. return "<table class='nobr'><caption>$caption</caption><tbody>";
  232. }
  233. protected static function table_row($property, $value)
  234. {
  235. if ($value) {
  236. return "<tr><td>$property:&nbsp;&nbsp;</td><td>$value</td></tr>";
  237. }
  238. return '';
  239. }
  240. protected static function table_tail()
  241. {
  242. return '</tbody></table>';
  243. }
  244. }
  245. class AudioSectionParser extends SectionParser
  246. {
  247. protected $audioformat;
  248. protected $audiobitrate;
  249. protected $audiochannels;
  250. protected $audiochannelpositions;
  251. protected $audiotitle;
  252. protected $audiolang;
  253. protected $audioprofile;
  254. protected $form_audioformat;
  255. protected $form_audiochannels;
  256. protected function handle_cases($property, $value)
  257. {
  258. switch ($property) {
  259. case "format":
  260. $this->audioformat = $value;
  261. break;
  262. case "bit rate":
  263. $this->audiobitrate = $value;
  264. break;
  265. case "channel(s)":
  266. $this->audiochannels = $value;
  267. break;
  268. case "channel positions":
  269. $this->audiochannelpositions = $value;
  270. break;
  271. case "title":
  272. $this->audiotitle = $value;
  273. break;
  274. case "language":
  275. $this->audiolang = $value;
  276. break;
  277. case "format profile":
  278. $this->audioprofile = $value;
  279. break;
  280. }
  281. }
  282. public function output()
  283. {
  284. $this->process_vars();
  285. $output = $this->audiolang . ' ' . $this->channels() . ' ' . $this->format();
  286. $output .= ($this->audiobitrate) ? " @ $this->audiobitrate" : '';
  287. $output .= ($this->audiotitle) ? " ($this->audiotitle)" : '';
  288. return $output;
  289. }
  290. public function output_raw()
  291. {
  292. $this->process_vars();
  293. $output = [];
  294. $properties = [
  295. 'audioformat', 'audiobitrate', 'audiochannels',
  296. 'audiochannelpositions', 'audiotitle', 'audiolang', 'audioprofile',
  297. 'form_audioformat', 'form_audiochannels'
  298. ];
  299. foreach ($properties as $property) {
  300. if ($this->$property) {
  301. $output[$property] = $this->$property;
  302. }
  303. }
  304. return $output;
  305. }
  306. protected function process_vars()
  307. {
  308. $this->form_audioformat = $this->form_format();
  309. $this->form_audiochannels = $this->form_channels();
  310. }
  311. protected function format()
  312. {
  313. if (strtolower($this->audioformat) === 'mpeg audio') {
  314. switch (strtolower($this->audioprofile)) {
  315. case 'layer 3':
  316. return 'MP3';
  317. case 'layer 2':
  318. return 'MP2';
  319. case 'layer 1':
  320. return 'MP1';
  321. }
  322. }
  323. return $this->audioformat;
  324. }
  325. protected function form_format()
  326. {
  327. // Not implemented: Real Audio, DTS-HD
  328. switch (strtolower($this->format())) {
  329. case 'mp2':
  330. return 'MP2';
  331. case 'mp3':
  332. return 'MP3';
  333. case 'vorbis':
  334. return 'OGG';
  335. case 'aac':
  336. return 'AAC';
  337. case 'ac-3':
  338. return 'AC3';
  339. case 'truehd':
  340. return 'TrueHD';
  341. case 'dts':
  342. switch (strtolower($this->audioprofile)) {
  343. case 'es':
  344. return 'DTS-ES';
  345. case 'ma / core':
  346. return 'DTS-HD MA';
  347. default:
  348. return 'DTS';
  349. }
  350. // no break
  351. case 'flac':
  352. return 'FLAC';
  353. case 'pcm':
  354. return 'PCM';
  355. case 'wma':
  356. return 'WMA';
  357. }
  358. }
  359. protected function channels()
  360. {
  361. if (isset($this->audiochannels)) {
  362. $chans = preg_replace('/^(\d).*$/', '$1', $this->audiochannels);
  363. if (isset($this->audiochannelpositions) && preg_match(
  364. '/LFE/',
  365. $this->audiochannelpositions
  366. )) {
  367. $chans -= .9;
  368. } else {
  369. $chans = $chans . '.0';
  370. }
  371. return $chans . 'ch';
  372. }
  373. }
  374. protected function form_channels()
  375. {
  376. return preg_replace('/ch/', '', $this->channels());
  377. }
  378. }
  379. class GeneralSectionParser extends SectionParser
  380. {
  381. public $filename;
  382. protected $generalformat;
  383. protected $duration;
  384. protected $filesize;
  385. protected $form_codec;
  386. protected $form_releasegroup;
  387. protected function handle_cases($property, $value)
  388. {
  389. switch ($property) {
  390. case "complete name":
  391. // Remove autodetected urls
  392. $value = preg_replace('#\[autourl(?:=.+)?\](.+)\[/autourl\]#', '$1', $value);
  393. $this->filename = static::strip_path($value);
  394. $this->lines[$this->index-1] = "Complete name : " . $this->filename;
  395. $this->filename = substr($this->filename, 0, 150);
  396. break;
  397. case "format":
  398. $this->generalformat = $value;
  399. break;
  400. case "duration":
  401. $this->duration = $value;
  402. break;
  403. case "file size":
  404. $this->filesize = $value;
  405. break;
  406. }
  407. }
  408. public function output()
  409. {
  410. $this->process_vars();
  411. $output = static::table_head('General');
  412. $properties = [
  413. 'Container' => 'generalformat',
  414. 'Runtime' => 'duration',
  415. 'Size' => 'filesize'
  416. ];
  417. foreach ($properties as $property => $value) {
  418. $output .= static::table_row($property, $this->$value);
  419. }
  420. $output .= static::table_tail();
  421. return $output;
  422. }
  423. public function output_raw()
  424. {
  425. $this->process_vars();
  426. $output = [];
  427. $properties = [
  428. 'filename', 'generalformat', 'duration', 'filesize', 'form_codec',
  429. 'form_releasegroup'
  430. ];
  431. foreach ($properties as $property) {
  432. if ($this->$property) {
  433. $output[$property] = $this->$property;
  434. }
  435. }
  436. return $output;
  437. }
  438. protected function process_vars()
  439. {
  440. switch (strtolower($this->generalformat)) {
  441. case 'mpeg-ts':
  442. $this->form_codec = 'MPEG-TS';
  443. break;
  444. // We can't determine if it's DVD5 or DVD9, so don't guess.
  445. case 'mpeg-ps':
  446. $this->form_codec = '---';
  447. break;
  448. }
  449. $matches = [];
  450. preg_match(
  451. '/(?:^|.*\\|\/)\[(.*?)\].*$/',
  452. $this->filename,
  453. $matches
  454. );
  455. $this->form_releasegroup = $matches ? $matches[1] : '';
  456. }
  457. }
  458. class TextSectionParser extends SectionParser
  459. {
  460. protected $title;
  461. protected $language;
  462. protected $format;
  463. protected $default;
  464. protected $processed_language;
  465. protected $form_format;
  466. protected function handle_cases($property, $value)
  467. {
  468. switch ($property) {
  469. case 'title':
  470. $this->title = $value;
  471. break;
  472. case 'language':
  473. $this->language = $value;
  474. break;
  475. case 'format':
  476. $this->format = $value;
  477. break;
  478. case 'default':
  479. $this->default = ($value == 'Yes');
  480. break;
  481. }
  482. }
  483. public function output()
  484. {
  485. $this->process_vars();
  486. $language = $this->processed_language;
  487. $output = "$language ($this->format)";
  488. if ($this->title) {
  489. $output .= " ($this->title)";
  490. }
  491. if ($this->default) {
  492. $output .= ' (default)';
  493. }
  494. return $output;
  495. }
  496. public function output_raw()
  497. {
  498. $this->process_vars();
  499. $output = [];
  500. $properties = [
  501. 'title', 'language', 'format', 'default', 'processed_language',
  502. 'form_format'
  503. ];
  504. foreach ($properties as $property) {
  505. if ($this->$property) {
  506. $output[$property] = $this->$property;
  507. }
  508. }
  509. return $output;
  510. }
  511. protected function process_vars()
  512. {
  513. $this->processed_language = ($this->language) ?
  514. $this->language : 'Unknown';
  515. $this->form_format = 'Softsubbed';
  516. }
  517. }
  518. class VideoSectionParser extends SectionParser
  519. {
  520. protected $videoformat;
  521. protected $videoformatversion;
  522. protected $codec;
  523. protected $width;
  524. protected $height;
  525. protected $writinglibrary;
  526. protected $frameratemode;
  527. protected $framerate;
  528. protected $aspectratio;
  529. protected $bitrate;
  530. protected $bitratemode;
  531. protected $nominalbitrate;
  532. protected $bpp;
  533. protected $bitdepth;
  534. protected $processed_codec;
  535. protected $processed_resolution;
  536. protected $processed_framerate;
  537. protected $form_codec;
  538. protected $form_resolution;
  539. protected function handle_cases($property, $value)
  540. {
  541. switch ($property) {
  542. case "format":
  543. $this->videoformat = $value;
  544. break;
  545. case "format version":
  546. $this->videoformatversion = $value;
  547. break;
  548. case "codec id":
  549. $this->codec = strtolower($value);
  550. break;
  551. case "width":
  552. $this->width = static::parse_size($value);
  553. break;
  554. case "height":
  555. $this->height = static::parse_size($value);
  556. break;
  557. case "writing library":
  558. $this->writinglibrary = $value;
  559. break;
  560. case "frame rate mode":
  561. $this->frameratemode = $value;
  562. break;
  563. case "frame rate":
  564. // if variable this becomes Original frame rate
  565. $this->framerate = $value;
  566. break;
  567. case "display aspect ratio":
  568. $this->aspectratio = $value;
  569. break;
  570. case "bit rate":
  571. $this->bitrate = $value;
  572. break;
  573. case "bit rate mode":
  574. $this->bitratemode = $value;
  575. break;
  576. case "nominal bit rate":
  577. $this->nominalbitrate = $value;
  578. break;
  579. case "bits/(pixel*frame)":
  580. $this->bpp = $value;
  581. break;
  582. case 'bit depth':
  583. $this->bitdepth = $value;
  584. break;
  585. }
  586. }
  587. public function output()
  588. {
  589. $this->process_vars();
  590. $output = static::table_head('Video');
  591. $properties = [
  592. 'Codec' => 'processed_codec',
  593. 'Bit depth' => 'bitdepth',
  594. 'Resolution' => 'processed_resolution',
  595. 'Aspect ratio' => 'aspectratio',
  596. 'Frame rate' => 'processed_framerate',
  597. 'Bit rate' => 'bitrate',
  598. 'BPP' => 'bpp'
  599. ];
  600. foreach ($properties as $property => $value) {
  601. $output .= static::table_row($property, $this->$value);
  602. }
  603. $output .= static::table_tail();
  604. return $output;
  605. }
  606. public function output_raw()
  607. {
  608. $this->process_vars();
  609. $output = [];
  610. $properties = [
  611. 'videoformat', 'videoformatversion', 'codec', 'width', 'height',
  612. 'writinglibrary', 'frameratemode', 'framerate', 'aspectratio',
  613. 'bitrate', 'bitratemode', 'nominalbitrate', 'bpp', 'bitdepth',
  614. 'processed_codec', 'processed_resolution', 'processed_framerate',
  615. 'form_codec', 'form_resolution'
  616. ];
  617. foreach ($properties as $property) {
  618. if ($this->$property) {
  619. $output[$property] = $this->$property;
  620. }
  621. }
  622. return $output;
  623. }
  624. protected function process_vars()
  625. {
  626. $this->processed_codec = $this->compute_codec();
  627. $this->processed_resolution = ($this->width) ?
  628. $this->width . 'x' . $this->height : '';
  629. $this->processed_framerate = (strtolower($this->frameratemode) !=
  630. "constant" && $this->frameratemode) ?
  631. $this->frameratemode : $this->framerate;
  632. $this->form_codec = $this->compute_form_codec();
  633. $this->form_resolution = $this->compute_form_resolution();
  634. }
  635. protected function compute_codec()
  636. {
  637. switch (strtolower($this->videoformat)) {
  638. case "mpeg video":
  639. switch (strtolower($this->videoformatversion)) {
  640. case "version 2":
  641. return "MPEG-2";
  642. case "version 1":
  643. return "MPEG-1";
  644. }
  645. return $this->videoformat;
  646. }
  647. switch (strtolower($this->codec)) {
  648. case "div3":
  649. return "DivX 3";
  650. case "divx":
  651. case "dx50":
  652. return "DivX";
  653. case "xvid":
  654. return "XviD";
  655. case "x264":
  656. return "x264";
  657. }
  658. $chk = strtolower($this->codec);
  659. $wl = strtolower($this->writinglibrary);
  660. if (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") === false) {
  661. return "H264";
  662. } elseif (($chk === "v_mpeg4/iso/avc" || $chk === "avc1") && strpos($wl, "x264 core") > -1) {
  663. return "x264";
  664. } elseif (strtolower($this->videoformat) === "avc" && strpos($wl, "x264 core") === false) {
  665. return "H264";
  666. }
  667. if (($chk === 'v_mpegh/iso/hevc') || ($wl === 'hevc')) {
  668. return 'H265';
  669. }
  670. }
  671. protected function compute_form_codec()
  672. {
  673. // Not implemented: DVD5, DVD9, WMV, Real Video
  674. // MPEG-TS set as GeneralSectionParser::$form_codec if found
  675. // MPEG-PS sets GeneralSectionParser::$form_codec to blank form value
  676. // so DVD5 or DVD9 is selected manually.
  677. $codec = $this->compute_codec();
  678. switch (strtolower($codec)) {
  679. case 'x264':
  680. case 'h264':
  681. return strtolower($this->bitdepth) == '10 bits' ?
  682. 'h264 10-bit' : 'h264';
  683. case 'h265':
  684. return 'h265';
  685. case 'xvid':
  686. return 'XviD';
  687. case 'divx':
  688. case 'divx 3':
  689. return 'DivX';
  690. case 'mpeg-1':
  691. return 'MPEG';
  692. case 'mpeg-2':
  693. return 'MPEG-2';
  694. }
  695. switch (strtolower($this->codec)) {
  696. case 'wmv3':
  697. return 'VC-1';
  698. case 'mp43':
  699. return 'MPEG-4 v3';
  700. }
  701. switch (strtolower($this->videoformat)) {
  702. case 'vc-1':
  703. return 'VC-1';
  704. case 's-mpeg 4 v3':
  705. return 'MPEG-4 v3';
  706. }
  707. }
  708. protected function compute_form_resolution()
  709. {
  710. global $Resolutions;
  711. $closest = null;
  712. if (isset($this->height)) {
  713. $resolutions = $Resolutions;
  714. foreach ($resolutions as $resolution) {
  715. if (!isset($closest) || abs($this->height - $resolution) <
  716. abs($this->height - $closest)) {
  717. $closest = $resolution;
  718. }
  719. }
  720. }
  721. return $closest;
  722. }
  723. }