FormatMetadata.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908
  1. <?php
  2. /**
  3. * Formatting of image metadata values into human readable form.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @ingroup Media
  21. * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
  22. * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason, 2009 Brent Garber, 2010 Brian Wolff
  23. * @license GPL-2.0-or-later
  24. * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
  25. * @file
  26. */
  27. use MediaWiki\MediaWikiServices;
  28. use Wikimedia\Timestamp\TimestampException;
  29. /**
  30. * Format Image metadata values into a human readable form.
  31. *
  32. * Note lots of these messages use the prefix 'exif' even though
  33. * they may not be exif properties. For example 'exif-ImageDescription'
  34. * can be the Exif ImageDescription, or it could be the iptc-iim caption
  35. * property, or it could be the xmp dc:description property. This
  36. * is because these messages should be independent of how the data is
  37. * stored, sine the user doesn't care if the description is stored in xmp,
  38. * exif, etc only that its a description. (Additionally many of these properties
  39. * are merged together following the MWG standard, such that for example,
  40. * exif properties override XMP properties that mean the same thing if
  41. * there is a conflict).
  42. *
  43. * It should perhaps use a prefix like 'metadata' instead, but there
  44. * is already a large number of messages using the 'exif' prefix.
  45. *
  46. * @ingroup Media
  47. * @since 1.23 the class extends ContextSource and various formerly-public
  48. * internal methods are private
  49. */
  50. class FormatMetadata extends ContextSource {
  51. /**
  52. * Only output a single language for multi-language fields
  53. * @var bool
  54. * @since 1.23
  55. */
  56. protected $singleLang = false;
  57. /**
  58. * Trigger only outputting single language for multilanguage fields
  59. *
  60. * @param bool $val
  61. * @since 1.23
  62. */
  63. public function setSingleLanguage( $val ) {
  64. $this->singleLang = $val;
  65. }
  66. /**
  67. * Numbers given by Exif user agents are often magical, that is they
  68. * should be replaced by a detailed explanation depending on their
  69. * value which most of the time are plain integers. This function
  70. * formats Exif (and other metadata) values into human readable form.
  71. *
  72. * This is the usual entry point for this class.
  73. *
  74. * @param array $tags The Exif data to format ( as returned by
  75. * Exif::getFilteredData() or BitmapMetadataHandler )
  76. * @param bool|IContextSource $context Context to use (optional)
  77. * @return array
  78. */
  79. public static function getFormattedData( $tags, $context = false ) {
  80. $obj = new FormatMetadata;
  81. if ( $context ) {
  82. $obj->setContext( $context );
  83. }
  84. return $obj->makeFormattedData( $tags );
  85. }
  86. /**
  87. * Numbers given by Exif user agents are often magical, that is they
  88. * should be replaced by a detailed explanation depending on their
  89. * value which most of the time are plain integers. This function
  90. * formats Exif (and other metadata) values into human readable form.
  91. *
  92. * @param array $tags The Exif data to format ( as returned by
  93. * Exif::getFilteredData() or BitmapMetadataHandler )
  94. * @return array
  95. * @since 1.23
  96. * @suppress PhanTypeArraySuspiciousNullable
  97. */
  98. public function makeFormattedData( $tags ) {
  99. $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
  100. unset( $tags['ResolutionUnit'] );
  101. foreach ( $tags as $tag => &$vals ) {
  102. // This seems ugly to wrap non-array's in an array just to unwrap again,
  103. // especially when most of the time it is not an array
  104. if ( !is_array( $tags[$tag] ) ) {
  105. $vals = [ $vals ];
  106. }
  107. // _type is a special value to say what array type
  108. if ( isset( $tags[$tag]['_type'] ) ) {
  109. $type = $tags[$tag]['_type'];
  110. unset( $vals['_type'] );
  111. } else {
  112. $type = 'ul'; // default unordered list.
  113. }
  114. // This is done differently as the tag is an array.
  115. if ( $tag == 'GPSTimeStamp' && count( $vals ) === 3 ) {
  116. // hour min sec array
  117. $h = explode( '/', $vals[0] );
  118. $m = explode( '/', $vals[1] );
  119. $s = explode( '/', $vals[2] );
  120. // this should already be validated
  121. // when loaded from file, but it could
  122. // come from a foreign repo, so be
  123. // paranoid.
  124. if ( !isset( $h[1] )
  125. || !isset( $m[1] )
  126. || !isset( $s[1] )
  127. || $h[1] == 0
  128. || $m[1] == 0
  129. || $s[1] == 0
  130. ) {
  131. continue;
  132. }
  133. $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT )
  134. . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT )
  135. . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT );
  136. try {
  137. $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] );
  138. // the 1971:01:01 is just a placeholder, and not shown to user.
  139. if ( $time && intval( $time ) > 0 ) {
  140. $tags[$tag] = $this->getLanguage()->time( $time );
  141. }
  142. } catch ( TimestampException $e ) {
  143. // This shouldn't happen, but we've seen bad formats
  144. // such as 4-digit seconds in the wild.
  145. // leave $tags[$tag] as-is
  146. }
  147. continue;
  148. }
  149. // The contact info is a multi-valued field
  150. // instead of the other props which are single
  151. // valued (mostly) so handle as a special case.
  152. if ( $tag === 'Contact' ) {
  153. $vals = $this->collapseContactInfo( $vals );
  154. continue;
  155. }
  156. foreach ( $vals as &$val ) {
  157. switch ( $tag ) {
  158. case 'Compression':
  159. switch ( $val ) {
  160. case 1:
  161. case 2:
  162. case 3:
  163. case 4:
  164. case 5:
  165. case 6:
  166. case 7:
  167. case 8:
  168. case 32773:
  169. case 32946:
  170. case 34712:
  171. $val = $this->exifMsg( $tag, $val );
  172. break;
  173. default:
  174. /* If not recognized, display as is. */
  175. break;
  176. }
  177. break;
  178. case 'PhotometricInterpretation':
  179. switch ( $val ) {
  180. case 0:
  181. case 1:
  182. case 2:
  183. case 3:
  184. case 4:
  185. case 5:
  186. case 6:
  187. case 8:
  188. case 9:
  189. case 10:
  190. case 32803:
  191. case 34892:
  192. $val = $this->exifMsg( $tag, $val );
  193. break;
  194. default:
  195. /* If not recognized, display as is. */
  196. break;
  197. }
  198. break;
  199. case 'Orientation':
  200. switch ( $val ) {
  201. case 1:
  202. case 2:
  203. case 3:
  204. case 4:
  205. case 5:
  206. case 6:
  207. case 7:
  208. case 8:
  209. $val = $this->exifMsg( $tag, $val );
  210. break;
  211. default:
  212. /* If not recognized, display as is. */
  213. break;
  214. }
  215. break;
  216. case 'PlanarConfiguration':
  217. switch ( $val ) {
  218. case 1:
  219. case 2:
  220. $val = $this->exifMsg( $tag, $val );
  221. break;
  222. default:
  223. /* If not recognized, display as is. */
  224. break;
  225. }
  226. break;
  227. // TODO: YCbCrSubSampling
  228. case 'YCbCrPositioning':
  229. switch ( $val ) {
  230. case 1:
  231. case 2:
  232. $val = $this->exifMsg( $tag, $val );
  233. break;
  234. default:
  235. /* If not recognized, display as is. */
  236. break;
  237. }
  238. break;
  239. case 'XResolution':
  240. case 'YResolution':
  241. switch ( $resolutionunit ) {
  242. case 2:
  243. $val = $this->exifMsg( 'XYResolution', 'i', $this->formatNum( $val ) );
  244. break;
  245. case 3:
  246. $val = $this->exifMsg( 'XYResolution', 'c', $this->formatNum( $val ) );
  247. break;
  248. default:
  249. /* If not recognized, display as is. */
  250. break;
  251. }
  252. break;
  253. // TODO: YCbCrCoefficients #p27 (see annex E)
  254. case 'ExifVersion':
  255. case 'FlashpixVersion':
  256. $val = (int)$val / 100;
  257. break;
  258. case 'ColorSpace':
  259. switch ( $val ) {
  260. case 1:
  261. case 65535:
  262. $val = $this->exifMsg( $tag, $val );
  263. break;
  264. default:
  265. /* If not recognized, display as is. */
  266. break;
  267. }
  268. break;
  269. case 'ComponentsConfiguration':
  270. switch ( $val ) {
  271. case 0:
  272. case 1:
  273. case 2:
  274. case 3:
  275. case 4:
  276. case 5:
  277. case 6:
  278. $val = $this->exifMsg( $tag, $val );
  279. break;
  280. default:
  281. /* If not recognized, display as is. */
  282. break;
  283. }
  284. break;
  285. case 'DateTime':
  286. case 'DateTimeOriginal':
  287. case 'DateTimeDigitized':
  288. case 'DateTimeReleased':
  289. case 'DateTimeExpires':
  290. case 'GPSDateStamp':
  291. case 'dc-date':
  292. case 'DateTimeMetadata':
  293. if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) {
  294. $val = $this->msg( 'exif-unknowndate' )->text();
  295. } elseif ( preg_match(
  296. '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D',
  297. $val
  298. ) ) {
  299. // Full date.
  300. $time = wfTimestamp( TS_MW, $val );
  301. if ( $time && intval( $time ) > 0 ) {
  302. $val = $this->getLanguage()->timeanddate( $time );
  303. }
  304. } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) {
  305. // No second field. Still format the same
  306. // since timeanddate doesn't include seconds anyways,
  307. // but second still available in api
  308. $time = wfTimestamp( TS_MW, $val . ':00' );
  309. if ( $time && intval( $time ) > 0 ) {
  310. $val = $this->getLanguage()->timeanddate( $time );
  311. }
  312. } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
  313. // If only the date but not the time is filled in.
  314. $time = wfTimestamp( TS_MW, substr( $val, 0, 4 )
  315. . substr( $val, 5, 2 )
  316. . substr( $val, 8, 2 )
  317. . '000000' );
  318. if ( $time && intval( $time ) > 0 ) {
  319. $val = $this->getLanguage()->date( $time );
  320. }
  321. }
  322. // else it will just output $val without formatting it.
  323. break;
  324. case 'ExposureProgram':
  325. switch ( $val ) {
  326. case 0:
  327. case 1:
  328. case 2:
  329. case 3:
  330. case 4:
  331. case 5:
  332. case 6:
  333. case 7:
  334. case 8:
  335. $val = $this->exifMsg( $tag, $val );
  336. break;
  337. default:
  338. /* If not recognized, display as is. */
  339. break;
  340. }
  341. break;
  342. case 'SubjectDistance':
  343. $val = $this->exifMsg( $tag, '', $this->formatNum( $val ) );
  344. break;
  345. case 'MeteringMode':
  346. switch ( $val ) {
  347. case 0:
  348. case 1:
  349. case 2:
  350. case 3:
  351. case 4:
  352. case 5:
  353. case 6:
  354. case 7:
  355. case 255:
  356. $val = $this->exifMsg( $tag, $val );
  357. break;
  358. default:
  359. /* If not recognized, display as is. */
  360. break;
  361. }
  362. break;
  363. case 'LightSource':
  364. switch ( $val ) {
  365. case 0:
  366. case 1:
  367. case 2:
  368. case 3:
  369. case 4:
  370. case 9:
  371. case 10:
  372. case 11:
  373. case 12:
  374. case 13:
  375. case 14:
  376. case 15:
  377. case 17:
  378. case 18:
  379. case 19:
  380. case 20:
  381. case 21:
  382. case 22:
  383. case 23:
  384. case 24:
  385. case 255:
  386. $val = $this->exifMsg( $tag, $val );
  387. break;
  388. default:
  389. /* If not recognized, display as is. */
  390. break;
  391. }
  392. break;
  393. case 'Flash':
  394. $flashDecode = [
  395. 'fired' => $val & 0b00000001,
  396. 'return' => ( $val & 0b00000110 ) >> 1,
  397. 'mode' => ( $val & 0b00011000 ) >> 3,
  398. 'function' => ( $val & 0b00100000 ) >> 5,
  399. 'redeye' => ( $val & 0b01000000 ) >> 6,
  400. // 'reserved' => ( $val & 0b10000000 ) >> 7,
  401. ];
  402. $flashMsgs = [];
  403. # We do not need to handle unknown values since all are used.
  404. foreach ( $flashDecode as $subTag => $subValue ) {
  405. # We do not need any message for zeroed values.
  406. if ( $subTag != 'fired' && $subValue == 0 ) {
  407. continue;
  408. }
  409. $fullTag = $tag . '-' . $subTag;
  410. $flashMsgs[] = $this->exifMsg( $fullTag, $subValue );
  411. }
  412. $val = $this->getLanguage()->commaList( $flashMsgs );
  413. break;
  414. case 'FocalPlaneResolutionUnit':
  415. switch ( $val ) {
  416. case 2:
  417. $val = $this->exifMsg( $tag, $val );
  418. break;
  419. default:
  420. /* If not recognized, display as is. */
  421. break;
  422. }
  423. break;
  424. case 'SensingMethod':
  425. switch ( $val ) {
  426. case 1:
  427. case 2:
  428. case 3:
  429. case 4:
  430. case 5:
  431. case 7:
  432. case 8:
  433. $val = $this->exifMsg( $tag, $val );
  434. break;
  435. default:
  436. /* If not recognized, display as is. */
  437. break;
  438. }
  439. break;
  440. case 'FileSource':
  441. switch ( $val ) {
  442. case 3:
  443. $val = $this->exifMsg( $tag, $val );
  444. break;
  445. default:
  446. /* If not recognized, display as is. */
  447. break;
  448. }
  449. break;
  450. case 'SceneType':
  451. switch ( $val ) {
  452. case 1:
  453. $val = $this->exifMsg( $tag, $val );
  454. break;
  455. default:
  456. /* If not recognized, display as is. */
  457. break;
  458. }
  459. break;
  460. case 'CustomRendered':
  461. switch ( $val ) {
  462. case 0: /* normal */
  463. case 1: /* custom */
  464. /* The following are unofficial Apple additions */
  465. case 2: /* HDR (no original saved) */
  466. case 3: /* HDR (original saved) */
  467. case 4: /* Original (for HDR) */
  468. /* Yes 5 is not present ;) */
  469. case 6: /* Panorama */
  470. case 7: /* Portrait HDR */
  471. case 8: /* Portrait */
  472. $val = $this->exifMsg( $tag, $val );
  473. break;
  474. default:
  475. /* If not recognized, display as is. */
  476. break;
  477. }
  478. break;
  479. case 'ExposureMode':
  480. switch ( $val ) {
  481. case 0:
  482. case 1:
  483. case 2:
  484. $val = $this->exifMsg( $tag, $val );
  485. break;
  486. default:
  487. /* If not recognized, display as is. */
  488. break;
  489. }
  490. break;
  491. case 'WhiteBalance':
  492. switch ( $val ) {
  493. case 0:
  494. case 1:
  495. $val = $this->exifMsg( $tag, $val );
  496. break;
  497. default:
  498. /* If not recognized, display as is. */
  499. break;
  500. }
  501. break;
  502. case 'SceneCaptureType':
  503. switch ( $val ) {
  504. case 0:
  505. case 1:
  506. case 2:
  507. case 3:
  508. $val = $this->exifMsg( $tag, $val );
  509. break;
  510. default:
  511. /* If not recognized, display as is. */
  512. break;
  513. }
  514. break;
  515. case 'GainControl':
  516. switch ( $val ) {
  517. case 0:
  518. case 1:
  519. case 2:
  520. case 3:
  521. case 4:
  522. $val = $this->exifMsg( $tag, $val );
  523. break;
  524. default:
  525. /* If not recognized, display as is. */
  526. break;
  527. }
  528. break;
  529. case 'Contrast':
  530. switch ( $val ) {
  531. case 0:
  532. case 1:
  533. case 2:
  534. $val = $this->exifMsg( $tag, $val );
  535. break;
  536. default:
  537. /* If not recognized, display as is. */
  538. break;
  539. }
  540. break;
  541. case 'Saturation':
  542. switch ( $val ) {
  543. case 0:
  544. case 1:
  545. case 2:
  546. $val = $this->exifMsg( $tag, $val );
  547. break;
  548. default:
  549. /* If not recognized, display as is. */
  550. break;
  551. }
  552. break;
  553. case 'Sharpness':
  554. switch ( $val ) {
  555. case 0:
  556. case 1:
  557. case 2:
  558. $val = $this->exifMsg( $tag, $val );
  559. break;
  560. default:
  561. /* If not recognized, display as is. */
  562. break;
  563. }
  564. break;
  565. case 'SubjectDistanceRange':
  566. switch ( $val ) {
  567. case 0:
  568. case 1:
  569. case 2:
  570. case 3:
  571. $val = $this->exifMsg( $tag, $val );
  572. break;
  573. default:
  574. /* If not recognized, display as is. */
  575. break;
  576. }
  577. break;
  578. // The GPS...Ref values are kept for compatibility, probably won't be reached.
  579. case 'GPSLatitudeRef':
  580. case 'GPSDestLatitudeRef':
  581. switch ( $val ) {
  582. case 'N':
  583. case 'S':
  584. $val = $this->exifMsg( 'GPSLatitude', $val );
  585. break;
  586. default:
  587. /* If not recognized, display as is. */
  588. break;
  589. }
  590. break;
  591. case 'GPSLongitudeRef':
  592. case 'GPSDestLongitudeRef':
  593. switch ( $val ) {
  594. case 'E':
  595. case 'W':
  596. $val = $this->exifMsg( 'GPSLongitude', $val );
  597. break;
  598. default:
  599. /* If not recognized, display as is. */
  600. break;
  601. }
  602. break;
  603. case 'GPSAltitude':
  604. if ( $val < 0 ) {
  605. $val = $this->exifMsg( 'GPSAltitude', 'below-sealevel', $this->formatNum( -$val, 3 ) );
  606. } else {
  607. $val = $this->exifMsg( 'GPSAltitude', 'above-sealevel', $this->formatNum( $val, 3 ) );
  608. }
  609. break;
  610. case 'GPSStatus':
  611. switch ( $val ) {
  612. case 'A':
  613. case 'V':
  614. $val = $this->exifMsg( $tag, $val );
  615. break;
  616. default:
  617. /* If not recognized, display as is. */
  618. break;
  619. }
  620. break;
  621. case 'GPSMeasureMode':
  622. switch ( $val ) {
  623. case 2:
  624. case 3:
  625. $val = $this->exifMsg( $tag, $val );
  626. break;
  627. default:
  628. /* If not recognized, display as is. */
  629. break;
  630. }
  631. break;
  632. case 'GPSTrackRef':
  633. case 'GPSImgDirectionRef':
  634. case 'GPSDestBearingRef':
  635. switch ( $val ) {
  636. case 'T':
  637. case 'M':
  638. $val = $this->exifMsg( 'GPSDirection', $val );
  639. break;
  640. default:
  641. /* If not recognized, display as is. */
  642. break;
  643. }
  644. break;
  645. case 'GPSLatitude':
  646. case 'GPSDestLatitude':
  647. $val = $this->formatCoords( $val, 'latitude' );
  648. break;
  649. case 'GPSLongitude':
  650. case 'GPSDestLongitude':
  651. $val = $this->formatCoords( $val, 'longitude' );
  652. break;
  653. case 'GPSSpeedRef':
  654. switch ( $val ) {
  655. case 'K':
  656. case 'M':
  657. case 'N':
  658. $val = $this->exifMsg( 'GPSSpeed', $val );
  659. break;
  660. default:
  661. /* If not recognized, display as is. */
  662. break;
  663. }
  664. break;
  665. case 'GPSDestDistanceRef':
  666. switch ( $val ) {
  667. case 'K':
  668. case 'M':
  669. case 'N':
  670. $val = $this->exifMsg( 'GPSDestDistance', $val );
  671. break;
  672. default:
  673. /* If not recognized, display as is. */
  674. break;
  675. }
  676. break;
  677. case 'GPSDOP':
  678. // See https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)
  679. if ( $val <= 2 ) {
  680. $val = $this->exifMsg( $tag, 'excellent', $this->formatNum( $val ) );
  681. } elseif ( $val <= 5 ) {
  682. $val = $this->exifMsg( $tag, 'good', $this->formatNum( $val ) );
  683. } elseif ( $val <= 10 ) {
  684. $val = $this->exifMsg( $tag, 'moderate', $this->formatNum( $val ) );
  685. } elseif ( $val <= 20 ) {
  686. $val = $this->exifMsg( $tag, 'fair', $this->formatNum( $val ) );
  687. } else {
  688. $val = $this->exifMsg( $tag, 'poor', $this->formatNum( $val ) );
  689. }
  690. break;
  691. // This is not in the Exif standard, just a special
  692. // case for our purposes which enables wikis to wikify
  693. // the make, model and software name to link to their articles.
  694. case 'Make':
  695. case 'Model':
  696. $val = $this->exifMsg( $tag, '', $val );
  697. break;
  698. case 'Software':
  699. if ( is_array( $val ) ) {
  700. if ( count( $val ) > 1 ) {
  701. // if its a software, version array.
  702. $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text();
  703. } else {
  704. // https://phabricator.wikimedia.org/T178130
  705. $val = $this->exifMsg( $tag, '', $val[0] );
  706. }
  707. } else {
  708. $val = $this->exifMsg( $tag, '', $val );
  709. }
  710. break;
  711. case 'ExposureTime':
  712. // Show the pretty fraction as well as decimal version
  713. $val = $this->msg( 'exif-exposuretime-format',
  714. $this->formatFraction( $val ), $this->formatNum( $val ) )->text();
  715. break;
  716. case 'ISOSpeedRatings':
  717. // If its = 65535 that means its at the
  718. // limit of the size of Exif::short and
  719. // is really higher.
  720. if ( $val == '65535' ) {
  721. $val = $this->exifMsg( $tag, 'overflow' );
  722. } else {
  723. $val = $this->formatNum( $val );
  724. }
  725. break;
  726. case 'FNumber':
  727. $val = $this->msg( 'exif-fnumber-format',
  728. $this->formatNum( $val ) )->text();
  729. break;
  730. case 'FocalLength':
  731. case 'FocalLengthIn35mmFilm':
  732. $val = $this->msg( 'exif-focallength-format',
  733. $this->formatNum( $val ) )->text();
  734. break;
  735. case 'MaxApertureValue':
  736. if ( strpos( $val, '/' ) !== false ) {
  737. // need to expand this earlier to calculate fNumber
  738. list( $n, $d ) = explode( '/', $val );
  739. if ( is_numeric( $n ) && is_numeric( $d ) ) {
  740. $val = $n / $d;
  741. }
  742. }
  743. if ( is_numeric( $val ) ) {
  744. $fNumber = 2 ** ( $val / 2 );
  745. if ( $fNumber !== false ) {
  746. $val = $this->msg( 'exif-maxaperturevalue-value',
  747. $this->formatNum( $val ),
  748. $this->formatNum( $fNumber, 2 )
  749. )->text();
  750. }
  751. }
  752. break;
  753. case 'iimCategory':
  754. switch ( strtolower( $val ) ) {
  755. // See pg 29 of IPTC photo
  756. // metadata standard.
  757. case 'ace':
  758. case 'clj':
  759. case 'dis':
  760. case 'fin':
  761. case 'edu':
  762. case 'evn':
  763. case 'hth':
  764. case 'hum':
  765. case 'lab':
  766. case 'lif':
  767. case 'pol':
  768. case 'rel':
  769. case 'sci':
  770. case 'soi':
  771. case 'spo':
  772. case 'war':
  773. case 'wea':
  774. $val = $this->exifMsg(
  775. 'iimcategory',
  776. $val
  777. );
  778. }
  779. break;
  780. case 'SubjectNewsCode':
  781. // Essentially like iimCategory.
  782. // 8 (numeric) digit hierarchical
  783. // classification. We decode the
  784. // first 2 digits, which provide
  785. // a broad category.
  786. $val = $this->convertNewsCode( $val );
  787. break;
  788. case 'Urgency':
  789. // 1-8 with 1 being highest, 5 normal
  790. // 0 is reserved, and 9 is 'user-defined'.
  791. $urgency = '';
  792. if ( $val == 0 || $val == 9 ) {
  793. $urgency = 'other';
  794. } elseif ( $val < 5 && $val > 1 ) {
  795. $urgency = 'high';
  796. } elseif ( $val == 5 ) {
  797. $urgency = 'normal';
  798. } elseif ( $val <= 8 && $val > 5 ) {
  799. $urgency = 'low';
  800. }
  801. if ( $urgency !== '' ) {
  802. $val = $this->exifMsg( 'urgency',
  803. $urgency, $val
  804. );
  805. }
  806. break;
  807. // Things that have a unit of pixels.
  808. case 'OriginalImageHeight':
  809. case 'OriginalImageWidth':
  810. case 'PixelXDimension':
  811. case 'PixelYDimension':
  812. case 'ImageWidth':
  813. case 'ImageLength':
  814. $val = $this->formatNum( $val ) . ' ' . $this->msg( 'unit-pixel' )->text();
  815. break;
  816. // Do not transform fields with pure text.
  817. // For some languages the formatNum()
  818. // conversion results to wrong output like
  819. // foo,bar@example,com or foo٫bar@example٫com.
  820. // Also some 'numeric' things like Scene codes
  821. // are included here as we really don't want
  822. // commas inserted.
  823. case 'ImageDescription':
  824. case 'UserComment':
  825. case 'Artist':
  826. case 'Copyright':
  827. case 'RelatedSoundFile':
  828. case 'ImageUniqueID':
  829. case 'SpectralSensitivity':
  830. case 'GPSSatellites':
  831. case 'GPSVersionID':
  832. case 'GPSMapDatum':
  833. case 'Keywords':
  834. case 'WorldRegionDest':
  835. case 'CountryDest':
  836. case 'CountryCodeDest':
  837. case 'ProvinceOrStateDest':
  838. case 'CityDest':
  839. case 'SublocationDest':
  840. case 'WorldRegionCreated':
  841. case 'CountryCreated':
  842. case 'CountryCodeCreated':
  843. case 'ProvinceOrStateCreated':
  844. case 'CityCreated':
  845. case 'SublocationCreated':
  846. case 'ObjectName':
  847. case 'SpecialInstructions':
  848. case 'Headline':
  849. case 'Credit':
  850. case 'Source':
  851. case 'EditStatus':
  852. case 'FixtureIdentifier':
  853. case 'LocationDest':
  854. case 'LocationDestCode':
  855. case 'Writer':
  856. case 'JPEGFileComment':
  857. case 'iimSupplementalCategory':
  858. case 'OriginalTransmissionRef':
  859. case 'Identifier':
  860. case 'dc-contributor':
  861. case 'dc-coverage':
  862. case 'dc-publisher':
  863. case 'dc-relation':
  864. case 'dc-rights':
  865. case 'dc-source':
  866. case 'dc-type':
  867. case 'Lens':
  868. case 'SerialNumber':
  869. case 'CameraOwnerName':
  870. case 'Label':
  871. case 'Nickname':
  872. case 'RightsCertificate':
  873. case 'CopyrightOwner':
  874. case 'UsageTerms':
  875. case 'WebStatement':
  876. case 'OriginalDocumentID':
  877. case 'LicenseUrl':
  878. case 'MorePermissionsUrl':
  879. case 'AttributionUrl':
  880. case 'PreferredAttributionName':
  881. case 'PNGFileComment':
  882. case 'Disclaimer':
  883. case 'ContentWarning':
  884. case 'GIFFileComment':
  885. case 'SceneCode':
  886. case 'IntellectualGenre':
  887. case 'Event':
  888. case 'OrginisationInImage':
  889. case 'PersonInImage':
  890. $val = htmlspecialchars( $val );
  891. break;
  892. case 'ObjectCycle':
  893. switch ( $val ) {
  894. case 'a':
  895. case 'p':
  896. case 'b':
  897. $val = $this->exifMsg( $tag, $val );
  898. break;
  899. default:
  900. $val = htmlspecialchars( $val );
  901. break;
  902. }
  903. break;
  904. case 'Copyrighted':
  905. switch ( $val ) {
  906. case 'True':
  907. case 'False':
  908. $val = $this->exifMsg( $tag, $val );
  909. break;
  910. }
  911. break;
  912. case 'Rating':
  913. if ( $val == '-1' ) {
  914. $val = $this->exifMsg( $tag, 'rejected' );
  915. } else {
  916. $val = $this->formatNum( $val );
  917. }
  918. break;
  919. case 'LanguageCode':
  920. $lang = Language::fetchLanguageName( strtolower( $val ), $this->getLanguage()->getCode() );
  921. $val = htmlspecialchars( $lang ?: $val );
  922. break;
  923. default:
  924. $val = $this->formatNum( $val );
  925. break;
  926. }
  927. }
  928. // End formatting values, start flattening arrays.
  929. $vals = $this->flattenArrayReal( $vals, $type );
  930. }
  931. return $tags;
  932. }
  933. /**
  934. * Flatten an array, using the content language for any messages.
  935. *
  936. * @param array $vals Array of values
  937. * @param string $type Type of array (either lang, ul, ol).
  938. * lang = language assoc array with keys being the lang code
  939. * ul = unordered list, ol = ordered list
  940. * type can also come from the '_type' member of $vals.
  941. * @param bool $noHtml If to avoid returning anything resembling HTML.
  942. * (Ugly hack for backwards compatibility with old MediaWiki).
  943. * @param bool|IContextSource $context
  944. * @return string Single value (in wiki-syntax).
  945. * @since 1.23
  946. */
  947. public static function flattenArrayContentLang( $vals, $type = 'ul',
  948. $noHtml = false, $context = false
  949. ) {
  950. $obj = new FormatMetadata;
  951. if ( $context ) {
  952. $obj->setContext( $context );
  953. }
  954. $context = new DerivativeContext( $obj->getContext() );
  955. $context->setLanguage( MediaWikiServices::getInstance()->getContentLanguage() );
  956. $obj->setContext( $context );
  957. return $obj->flattenArrayReal( $vals, $type, $noHtml );
  958. }
  959. /**
  960. * A function to collapse multivalued tags into a single value.
  961. * This turns an array of (for example) authors into a bulleted list.
  962. *
  963. * This is public on the basis it might be useful outside of this class.
  964. *
  965. * @param array $vals Array of values
  966. * @param string $type Type of array (either lang, ul, ol).
  967. * lang = language assoc array with keys being the lang code
  968. * ul = unordered list, ol = ordered list
  969. * type can also come from the '_type' member of $vals.
  970. * @param bool $noHtml If to avoid returning anything resembling HTML.
  971. * (Ugly hack for backwards compatibility with old mediawiki).
  972. * @return string Single value (in wiki-syntax).
  973. * @since 1.23
  974. */
  975. public function flattenArrayReal( $vals, $type = 'ul', $noHtml = false ) {
  976. if ( !is_array( $vals ) ) {
  977. return $vals; // do nothing if not an array;
  978. }
  979. if ( isset( $vals['_type'] ) ) {
  980. $type = $vals['_type'];
  981. unset( $vals['_type'] );
  982. }
  983. if ( !is_array( $vals ) ) {
  984. return $vals; // do nothing if not an array;
  985. } elseif ( count( $vals ) === 1 && $type !== 'lang' && isset( $vals[0] ) ) {
  986. return $vals[0];
  987. } elseif ( count( $vals ) === 0 ) {
  988. wfDebug( __METHOD__ . " metadata array with 0 elements!\n" );
  989. return ""; // paranoia. This should never happen
  990. } else {
  991. /* @todo FIXME: This should hide some of the list entries if there are
  992. * say more than four. Especially if a field is translated into 20
  993. * languages, we don't want to show them all by default
  994. */
  995. switch ( $type ) {
  996. case 'lang':
  997. // Display default, followed by ContentLanguage,
  998. // followed by the rest in no particular
  999. // order.
  1000. // Todo: hide some items if really long list.
  1001. $content = '';
  1002. $priorityLanguages = $this->getPriorityLanguages();
  1003. $defaultItem = false;
  1004. $defaultLang = false;
  1005. // If default is set, save it for later,
  1006. // as we don't know if it's equal to
  1007. // one of the lang codes. (In xmp
  1008. // you specify the language for a
  1009. // default property by having both
  1010. // a default prop, and one in the language
  1011. // that are identical)
  1012. if ( isset( $vals['x-default'] ) ) {
  1013. $defaultItem = $vals['x-default'];
  1014. unset( $vals['x-default'] );
  1015. }
  1016. foreach ( $priorityLanguages as $pLang ) {
  1017. if ( isset( $vals[$pLang] ) ) {
  1018. $isDefault = false;
  1019. if ( $vals[$pLang] === $defaultItem ) {
  1020. $defaultItem = false;
  1021. $isDefault = true;
  1022. }
  1023. $content .= $this->langItem(
  1024. $vals[$pLang], $pLang,
  1025. $isDefault, $noHtml );
  1026. unset( $vals[$pLang] );
  1027. if ( $this->singleLang ) {
  1028. return Html::rawElement( 'span',
  1029. [ 'lang' => $pLang ], $vals[$pLang] );
  1030. }
  1031. }
  1032. }
  1033. // Now do the rest.
  1034. foreach ( $vals as $lang => $item ) {
  1035. if ( $item === $defaultItem ) {
  1036. $defaultLang = $lang;
  1037. continue;
  1038. }
  1039. $content .= $this->langItem( $item,
  1040. $lang, false, $noHtml );
  1041. if ( $this->singleLang ) {
  1042. return Html::rawElement( 'span',
  1043. [ 'lang' => $lang ], $item );
  1044. }
  1045. }
  1046. if ( $defaultItem !== false ) {
  1047. $content = $this->langItem( $defaultItem,
  1048. $defaultLang, true, $noHtml ) .
  1049. $content;
  1050. if ( $this->singleLang ) {
  1051. return $defaultItem;
  1052. }
  1053. }
  1054. if ( $noHtml ) {
  1055. return $content;
  1056. }
  1057. return '<ul class="metadata-langlist">' .
  1058. $content .
  1059. '</ul>';
  1060. case 'ol':
  1061. if ( $noHtml ) {
  1062. return "\n#" . implode( "\n#", $vals );
  1063. }
  1064. return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>';
  1065. case 'ul':
  1066. default:
  1067. if ( $noHtml ) {
  1068. return "\n*" . implode( "\n*", $vals );
  1069. }
  1070. return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>';
  1071. }
  1072. }
  1073. }
  1074. /** Helper function for creating lists of translations.
  1075. *
  1076. * @param string $value Value (this is not escaped)
  1077. * @param string $lang Lang code of item or false
  1078. * @param bool $default If it is default value.
  1079. * @param bool $noHtml If to avoid html (for back-compat)
  1080. * @throws MWException
  1081. * @return string Language item (Note: despite how this looks, this is
  1082. * treated as wikitext, not as HTML).
  1083. */
  1084. private function langItem( $value, $lang, $default = false, $noHtml = false ) {
  1085. if ( $lang === false && $default === false ) {
  1086. throw new MWException( '$lang and $default cannot both '
  1087. . 'be false.' );
  1088. }
  1089. if ( $noHtml ) {
  1090. $wrappedValue = $value;
  1091. } else {
  1092. $wrappedValue = '<span class="mw-metadata-lang-value">'
  1093. . $value . '</span>';
  1094. }
  1095. if ( $lang === false ) {
  1096. $msg = $this->msg( 'metadata-langitem-default', $wrappedValue );
  1097. if ( $noHtml ) {
  1098. return $msg->text() . "\n\n";
  1099. } /* else */
  1100. return '<li class="mw-metadata-lang-default">'
  1101. . $msg->text()
  1102. . "</li>\n";
  1103. }
  1104. $lowLang = strtolower( $lang );
  1105. $langName = Language::fetchLanguageName( $lowLang );
  1106. if ( $langName === '' ) {
  1107. // try just the base language name. (aka en-US -> en ).
  1108. $langPrefix = explode( '-', $lowLang, 2 )[0];
  1109. $langName = Language::fetchLanguageName( $langPrefix );
  1110. if ( $langName === '' ) {
  1111. // give up.
  1112. $langName = $lang;
  1113. }
  1114. }
  1115. // else we have a language specified
  1116. $msg = $this->msg( 'metadata-langitem', $wrappedValue, $langName, $lang );
  1117. if ( $noHtml ) {
  1118. return '*' . $msg->text();
  1119. } /* else: */
  1120. $item = '<li class="mw-metadata-lang-code-'
  1121. . $lang;
  1122. if ( $default ) {
  1123. $item .= ' mw-metadata-lang-default';
  1124. }
  1125. $item .= '" lang="' . $lang . '">';
  1126. $item .= $msg->text();
  1127. $item .= "</li>\n";
  1128. return $item;
  1129. }
  1130. /**
  1131. * Convenience function for getFormattedData()
  1132. *
  1133. * @param string $tag The tag name to pass on
  1134. * @param string $val The value of the tag
  1135. * @param string $arg An argument to pass ($1)
  1136. * @param string $arg2 A 2nd argument to pass ($2)
  1137. * @return string The text content of "exif-$tag-$val" message in lower case
  1138. */
  1139. private function exifMsg( $tag, $val, $arg = null, $arg2 = null ) {
  1140. if ( $val === '' ) {
  1141. $val = 'value';
  1142. }
  1143. return $this->msg(
  1144. MediaWikiServices::getInstance()->getContentLanguage()->lc( "exif-$tag-$val" ),
  1145. $arg,
  1146. $arg2
  1147. )->text();
  1148. }
  1149. /**
  1150. * Format a number, convert numbers from fractions into floating point
  1151. * numbers, joins arrays of numbers with commas.
  1152. *
  1153. * @param mixed $num The value to format
  1154. * @param float|int|bool $round Digits to round to or false.
  1155. * @return mixed A floating point number or whatever we were fed
  1156. */
  1157. private function formatNum( $num, $round = false ) {
  1158. $m = [];
  1159. if ( is_array( $num ) ) {
  1160. $out = [];
  1161. foreach ( $num as $number ) {
  1162. $out[] = $this->formatNum( $number );
  1163. }
  1164. return $this->getLanguage()->commaList( $out );
  1165. }
  1166. if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
  1167. if ( $m[2] != 0 ) {
  1168. $newNum = $m[1] / $m[2];
  1169. if ( $round !== false ) {
  1170. $newNum = round( $newNum, $round );
  1171. }
  1172. } else {
  1173. $newNum = $num;
  1174. }
  1175. return $this->getLanguage()->formatNum( $newNum );
  1176. } else {
  1177. if ( is_numeric( $num ) && $round !== false ) {
  1178. $num = round( $num, $round );
  1179. }
  1180. return $this->getLanguage()->formatNum( $num );
  1181. }
  1182. }
  1183. /**
  1184. * Format a rational number, reducing fractions
  1185. *
  1186. * @param mixed $num The value to format
  1187. * @return mixed A floating point number or whatever we were fed
  1188. */
  1189. private function formatFraction( $num ) {
  1190. $m = [];
  1191. if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
  1192. $numerator = intval( $m[1] );
  1193. $denominator = intval( $m[2] );
  1194. $gcd = $this->gcd( abs( $numerator ), $denominator );
  1195. if ( $gcd != 0 ) {
  1196. // 0 shouldn't happen! ;)
  1197. return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd );
  1198. }
  1199. }
  1200. return $this->formatNum( $num );
  1201. }
  1202. /**
  1203. * Calculate the greatest common divisor of two integers.
  1204. *
  1205. * @param int $a Numerator
  1206. * @param int $b Denominator
  1207. * @return int
  1208. */
  1209. private function gcd( $a, $b ) {
  1210. /*
  1211. // https://en.wikipedia.org/wiki/Euclidean_algorithm
  1212. // Recursive form would be:
  1213. if( $b == 0 )
  1214. return $a;
  1215. else
  1216. return gcd( $b, $a % $b );
  1217. */
  1218. while ( $b != 0 ) {
  1219. $remainder = $a % $b;
  1220. // tail recursion...
  1221. $a = $b;
  1222. $b = $remainder;
  1223. }
  1224. return $a;
  1225. }
  1226. /**
  1227. * Fetch the human readable version of a news code.
  1228. * A news code is an 8 digit code. The first two
  1229. * digits are a general classification, so we just
  1230. * translate that.
  1231. *
  1232. * Note, leading 0's are significant, so this is
  1233. * a string, not an int.
  1234. *
  1235. * @param string $val The 8 digit news code.
  1236. * @return string The human readable form
  1237. */
  1238. private function convertNewsCode( $val ) {
  1239. if ( !preg_match( '/^\d{8}$/D', $val ) ) {
  1240. // Not a valid news code.
  1241. return $val;
  1242. }
  1243. $cat = '';
  1244. switch ( substr( $val, 0, 2 ) ) {
  1245. case '01':
  1246. $cat = 'ace';
  1247. break;
  1248. case '02':
  1249. $cat = 'clj';
  1250. break;
  1251. case '03':
  1252. $cat = 'dis';
  1253. break;
  1254. case '04':
  1255. $cat = 'fin';
  1256. break;
  1257. case '05':
  1258. $cat = 'edu';
  1259. break;
  1260. case '06':
  1261. $cat = 'evn';
  1262. break;
  1263. case '07':
  1264. $cat = 'hth';
  1265. break;
  1266. case '08':
  1267. $cat = 'hum';
  1268. break;
  1269. case '09':
  1270. $cat = 'lab';
  1271. break;
  1272. case '10':
  1273. $cat = 'lif';
  1274. break;
  1275. case '11':
  1276. $cat = 'pol';
  1277. break;
  1278. case '12':
  1279. $cat = 'rel';
  1280. break;
  1281. case '13':
  1282. $cat = 'sci';
  1283. break;
  1284. case '14':
  1285. $cat = 'soi';
  1286. break;
  1287. case '15':
  1288. $cat = 'spo';
  1289. break;
  1290. case '16':
  1291. $cat = 'war';
  1292. break;
  1293. case '17':
  1294. $cat = 'wea';
  1295. break;
  1296. }
  1297. if ( $cat !== '' ) {
  1298. $catMsg = $this->exifMsg( 'iimcategory', $cat );
  1299. $val = $this->exifMsg( 'subjectnewscode', '', $val, $catMsg );
  1300. }
  1301. return $val;
  1302. }
  1303. /**
  1304. * Format a coordinate value, convert numbers from floating point
  1305. * into degree minute second representation.
  1306. *
  1307. * @param float|string $coord Expected to be a number or numeric string in degrees
  1308. * @param string $type "latitude" or "longitude"
  1309. * @return string
  1310. */
  1311. private function formatCoords( $coord, string $type ) {
  1312. if ( !is_numeric( $coord ) ) {
  1313. wfDebugLog( 'exif', __METHOD__ . ": \"$coord\" is not a number" );
  1314. return (string)$coord;
  1315. }
  1316. $ref = '';
  1317. if ( $coord < 0 ) {
  1318. $nCoord = -$coord;
  1319. if ( $type === 'latitude' ) {
  1320. $ref = 'S';
  1321. } elseif ( $type === 'longitude' ) {
  1322. $ref = 'W';
  1323. }
  1324. } else {
  1325. $nCoord = (float)$coord;
  1326. if ( $type === 'latitude' ) {
  1327. $ref = 'N';
  1328. } elseif ( $type === 'longitude' ) {
  1329. $ref = 'E';
  1330. }
  1331. }
  1332. $deg = floor( $nCoord );
  1333. $min = floor( ( $nCoord - $deg ) * 60 );
  1334. $sec = round( ( ( $nCoord - $deg ) * 60 - $min ) * 60, 2 );
  1335. $deg = $this->formatNum( $deg );
  1336. $min = $this->formatNum( $min );
  1337. $sec = $this->formatNum( $sec );
  1338. // Note the default message "$1° $2′ $3″ $4" ignores the 5th parameter
  1339. return $this->msg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text();
  1340. }
  1341. /**
  1342. * Format the contact info field into a single value.
  1343. *
  1344. * This function might be called from
  1345. * JpegHandler::convertMetadataVersion which is why it is
  1346. * public.
  1347. *
  1348. * @param array $vals Array with fields of the ContactInfo
  1349. * struct defined in the IPTC4XMP spec. Or potentially
  1350. * an array with one element that is a free form text
  1351. * value from the older iptc iim 1:118 prop.
  1352. * @return string HTML-ish looking wikitext
  1353. * @since 1.23 no longer static
  1354. */
  1355. public function collapseContactInfo( $vals ) {
  1356. if ( !( isset( $vals['CiAdrExtadr'] )
  1357. || isset( $vals['CiAdrCity'] )
  1358. || isset( $vals['CiAdrCtry'] )
  1359. || isset( $vals['CiEmailWork'] )
  1360. || isset( $vals['CiTelWork'] )
  1361. || isset( $vals['CiAdrPcode'] )
  1362. || isset( $vals['CiAdrRegion'] )
  1363. || isset( $vals['CiUrlWork'] )
  1364. ) ) {
  1365. // We don't have any sub-properties
  1366. // This could happen if its using old
  1367. // iptc that just had this as a free-form
  1368. // text value.
  1369. // Note: We run this through htmlspecialchars
  1370. // partially to be consistent, and partially
  1371. // because people often insert >, etc into
  1372. // the metadata which should not be interpreted
  1373. // but we still want to auto-link urls.
  1374. foreach ( $vals as &$val ) {
  1375. $val = htmlspecialchars( $val );
  1376. }
  1377. return $this->flattenArrayReal( $vals );
  1378. } else {
  1379. // We have a real ContactInfo field.
  1380. // Its unclear if all these fields have to be
  1381. // set, so assume they do not.
  1382. $url = $tel = $street = $city = $country = '';
  1383. $email = $postal = $region = '';
  1384. // Also note, some of the class names this uses
  1385. // are similar to those used by hCard. This is
  1386. // mostly because they're sensible names. This
  1387. // does not (and does not attempt to) output
  1388. // stuff in the hCard microformat. However it
  1389. // might output in the adr microformat.
  1390. if ( isset( $vals['CiAdrExtadr'] ) ) {
  1391. // Todo: This can potentially be multi-line.
  1392. // Need to check how that works in XMP.
  1393. $street = '<span class="extended-address">'
  1394. . htmlspecialchars(
  1395. $vals['CiAdrExtadr'] )
  1396. . '</span>';
  1397. }
  1398. if ( isset( $vals['CiAdrCity'] ) ) {
  1399. $city = '<span class="locality">'
  1400. . htmlspecialchars( $vals['CiAdrCity'] )
  1401. . '</span>';
  1402. }
  1403. if ( isset( $vals['CiAdrCtry'] ) ) {
  1404. $country = '<span class="country-name">'
  1405. . htmlspecialchars( $vals['CiAdrCtry'] )
  1406. . '</span>';
  1407. }
  1408. if ( isset( $vals['CiEmailWork'] ) ) {
  1409. $emails = [];
  1410. // Have to split multiple emails at commas/new lines.
  1411. $splitEmails = explode( "\n", $vals['CiEmailWork'] );
  1412. foreach ( $splitEmails as $e1 ) {
  1413. // Also split on comma
  1414. foreach ( explode( ',', $e1 ) as $e2 ) {
  1415. $finalEmail = trim( $e2 );
  1416. if ( $finalEmail == ',' || $finalEmail == '' ) {
  1417. continue;
  1418. }
  1419. if ( strpos( $finalEmail, '<' ) !== false ) {
  1420. // Don't do fancy formatting to
  1421. // "My name" <foo@bar.com> style stuff
  1422. $emails[] = $finalEmail;
  1423. } else {
  1424. $emails[] = '[mailto:'
  1425. . $finalEmail
  1426. . ' <span class="email">'
  1427. . $finalEmail
  1428. . '</span>]';
  1429. }
  1430. }
  1431. }
  1432. $email = implode( ', ', $emails );
  1433. }
  1434. if ( isset( $vals['CiTelWork'] ) ) {
  1435. $tel = '<span class="tel">'
  1436. . htmlspecialchars( $vals['CiTelWork'] )
  1437. . '</span>';
  1438. }
  1439. if ( isset( $vals['CiAdrPcode'] ) ) {
  1440. $postal = '<span class="postal-code">'
  1441. . htmlspecialchars(
  1442. $vals['CiAdrPcode'] )
  1443. . '</span>';
  1444. }
  1445. if ( isset( $vals['CiAdrRegion'] ) ) {
  1446. // Note this is province/state.
  1447. $region = '<span class="region">'
  1448. . htmlspecialchars(
  1449. $vals['CiAdrRegion'] )
  1450. . '</span>';
  1451. }
  1452. if ( isset( $vals['CiUrlWork'] ) ) {
  1453. $url = '<span class="url">'
  1454. . htmlspecialchars( $vals['CiUrlWork'] )
  1455. . '</span>';
  1456. }
  1457. return $this->msg( 'exif-contact-value', $email, $url,
  1458. $street, $city, $region, $postal, $country,
  1459. $tel )->text();
  1460. }
  1461. }
  1462. /**
  1463. * Get a list of fields that are visible by default.
  1464. *
  1465. * @return array
  1466. * @since 1.23
  1467. */
  1468. public static function getVisibleFields() {
  1469. $fields = [];
  1470. $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() );
  1471. foreach ( $lines as $line ) {
  1472. $matches = [];
  1473. if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
  1474. $fields[] = $matches[1];
  1475. }
  1476. }
  1477. $fields = array_map( 'strtolower', $fields );
  1478. return $fields;
  1479. }
  1480. /**
  1481. * Get an array of extended metadata. (See the imageinfo API for format.)
  1482. *
  1483. * @param File $file File to use
  1484. * @return array [<property name> => ['value' => <value>]], or [] on error
  1485. * @since 1.23
  1486. */
  1487. public function fetchExtendedMetadata( File $file ) {
  1488. $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
  1489. // If revision deleted, exit immediately
  1490. if ( $file->isDeleted( File::DELETED_FILE ) ) {
  1491. return [];
  1492. }
  1493. $cacheKey = $cache->makeKey(
  1494. 'getExtendedMetadata',
  1495. $this->getLanguage()->getCode(),
  1496. (int)$this->singleLang,
  1497. $file->getSha1()
  1498. );
  1499. $cachedValue = $cache->get( $cacheKey );
  1500. if (
  1501. $cachedValue
  1502. && Hooks::run( 'ValidateExtendedMetadataCache', [ $cachedValue['timestamp'], $file ] )
  1503. ) {
  1504. $extendedMetadata = $cachedValue['data'];
  1505. } else {
  1506. $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30;
  1507. $fileMetadata = $this->getExtendedMetadataFromFile( $file );
  1508. $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime );
  1509. if ( $this->singleLang ) {
  1510. $this->resolveMultilangMetadata( $extendedMetadata );
  1511. }
  1512. $this->discardMultipleValues( $extendedMetadata );
  1513. // Make sure the metadata won't break the API when an XML format is used.
  1514. // This is an API-specific function so it would be cleaner to call it from
  1515. // outside fetchExtendedMetadata, but this way we don't need to redo the
  1516. // computation on a cache hit.
  1517. $this->sanitizeArrayForAPI( $extendedMetadata );
  1518. $valueToCache = [ 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() ];
  1519. $cache->set( $cacheKey, $valueToCache, $maxCacheTime );
  1520. }
  1521. return $extendedMetadata;
  1522. }
  1523. /**
  1524. * Get file-based metadata in standardized format.
  1525. *
  1526. * Note that for a remote file, this might return metadata supplied by extensions.
  1527. *
  1528. * @param File $file File to use
  1529. * @return array [<property name> => ['value' => <value>]], or [] on error
  1530. * @since 1.23
  1531. */
  1532. protected function getExtendedMetadataFromFile( File $file ) {
  1533. // If this is a remote file accessed via an API request, we already
  1534. // have remote metadata so we just ignore any local one
  1535. if ( $file instanceof ForeignAPIFile ) {
  1536. // In case of error we pretend no metadata - this will get cached.
  1537. // Might or might not be a good idea.
  1538. return $file->getExtendedMetadata() ?: [];
  1539. }
  1540. $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
  1541. $fileMetadata = [
  1542. // This is modification time, which is close to "upload" time.
  1543. 'DateTime' => [
  1544. 'value' => $uploadDate,
  1545. 'source' => 'mediawiki-metadata',
  1546. ],
  1547. ];
  1548. $title = $file->getTitle();
  1549. if ( $title ) {
  1550. $text = $title->getText();
  1551. $pos = strrpos( $text, '.' );
  1552. if ( $pos ) {
  1553. $name = substr( $text, 0, $pos );
  1554. } else {
  1555. $name = $text;
  1556. }
  1557. $fileMetadata['ObjectName'] = [
  1558. 'value' => $name,
  1559. 'source' => 'mediawiki-metadata',
  1560. ];
  1561. }
  1562. return $fileMetadata;
  1563. }
  1564. /**
  1565. * Get additional metadata from hooks in standardized format.
  1566. *
  1567. * @param File $file File to use
  1568. * @param array $extendedMetadata
  1569. * @param int &$maxCacheTime Hook handlers might use this parameter to override cache time
  1570. *
  1571. * @return array [<property name> => ['value' => <value>]], or [] on error
  1572. * @since 1.23
  1573. */
  1574. protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata,
  1575. &$maxCacheTime
  1576. ) {
  1577. Hooks::run( 'GetExtendedMetadata', [
  1578. &$extendedMetadata,
  1579. $file,
  1580. $this->getContext(),
  1581. $this->singleLang,
  1582. &$maxCacheTime
  1583. ] );
  1584. $visible = array_flip( self::getVisibleFields() );
  1585. foreach ( $extendedMetadata as $key => $value ) {
  1586. if ( !isset( $visible[strtolower( $key )] ) ) {
  1587. $extendedMetadata[$key]['hidden'] = '';
  1588. }
  1589. }
  1590. return $extendedMetadata;
  1591. }
  1592. /**
  1593. * Turns an XMP-style multilang array into a single value.
  1594. * If the value is not a multilang array, it is returned unchanged.
  1595. * See mediawiki.org/wiki/Manual:File_metadata_handling#Multi-language_array_format
  1596. * @param mixed $value
  1597. * @return mixed Value in best language, null if there were no languages at all
  1598. * @since 1.23
  1599. */
  1600. protected function resolveMultilangValue( $value ) {
  1601. if (
  1602. !is_array( $value )
  1603. || !isset( $value['_type'] )
  1604. || $value['_type'] != 'lang'
  1605. ) {
  1606. return $value; // do nothing if not a multilang array
  1607. }
  1608. // choose the language best matching user or site settings
  1609. $priorityLanguages = $this->getPriorityLanguages();
  1610. foreach ( $priorityLanguages as $lang ) {
  1611. if ( isset( $value[$lang] ) ) {
  1612. return $value[$lang];
  1613. }
  1614. }
  1615. // otherwise go with the default language, if set
  1616. if ( isset( $value['x-default'] ) ) {
  1617. return $value['x-default'];
  1618. }
  1619. // otherwise just return any one language
  1620. unset( $value['_type'] );
  1621. if ( !empty( $value ) ) {
  1622. return reset( $value );
  1623. }
  1624. // this should not happen; signal error
  1625. return null;
  1626. }
  1627. /**
  1628. * Turns an XMP-style multivalue array into a single value by dropping all but the first
  1629. * value. If the value is not a multivalue array (or a multivalue array inside a multilang
  1630. * array), it is returned unchanged.
  1631. * See mediawiki.org/wiki/Manual:File_metadata_handling#Multi-language_array_format
  1632. * @param mixed $value
  1633. * @return mixed The value, or the first value if there were multiple ones
  1634. * @since 1.25
  1635. */
  1636. protected function resolveMultivalueValue( $value ) {
  1637. if ( !is_array( $value ) ) {
  1638. return $value;
  1639. } elseif ( isset( $value['_type'] ) && $value['_type'] === 'lang' ) {
  1640. // if this is a multilang array, process fields separately
  1641. $newValue = [];
  1642. foreach ( $value as $k => $v ) {
  1643. $newValue[$k] = $this->resolveMultivalueValue( $v );
  1644. }
  1645. return $newValue;
  1646. } else { // _type is 'ul' or 'ol' or missing in which case it defaults to 'ul'
  1647. $v = reset( $value );
  1648. if ( key( $value ) === '_type' ) {
  1649. $v = next( $value );
  1650. }
  1651. return $v;
  1652. }
  1653. }
  1654. /**
  1655. * Takes an array returned by the getExtendedMetadata* functions,
  1656. * and resolves multi-language values in it.
  1657. * @param array &$metadata
  1658. * @since 1.23
  1659. */
  1660. protected function resolveMultilangMetadata( &$metadata ) {
  1661. if ( !is_array( $metadata ) ) {
  1662. return;
  1663. }
  1664. foreach ( $metadata as &$field ) {
  1665. if ( isset( $field['value'] ) ) {
  1666. $field['value'] = $this->resolveMultilangValue( $field['value'] );
  1667. }
  1668. }
  1669. }
  1670. /**
  1671. * Takes an array returned by the getExtendedMetadata* functions,
  1672. * and turns all fields into single-valued ones by dropping extra values.
  1673. * @param array &$metadata
  1674. * @since 1.25
  1675. */
  1676. protected function discardMultipleValues( &$metadata ) {
  1677. if ( !is_array( $metadata ) ) {
  1678. return;
  1679. }
  1680. foreach ( $metadata as $key => &$field ) {
  1681. if ( $key === 'Software' || $key === 'Contact' ) {
  1682. // we skip some fields which have composite values. They are not particularly interesting
  1683. // and you can get them via the metadata / commonmetadata APIs anyway.
  1684. continue;
  1685. }
  1686. if ( isset( $field['value'] ) ) {
  1687. $field['value'] = $this->resolveMultivalueValue( $field['value'] );
  1688. }
  1689. }
  1690. }
  1691. /**
  1692. * Makes sure the given array is a valid API response fragment
  1693. * @param array &$arr
  1694. */
  1695. protected function sanitizeArrayForAPI( &$arr ) {
  1696. if ( !is_array( $arr ) ) {
  1697. return;
  1698. }
  1699. $counter = 1;
  1700. foreach ( $arr as $key => &$value ) {
  1701. $sanitizedKey = $this->sanitizeKeyForAPI( $key );
  1702. if ( $sanitizedKey !== $key ) {
  1703. if ( isset( $arr[$sanitizedKey] ) ) {
  1704. // Make the sanitized keys hopefully unique.
  1705. // To make it definitely unique would be too much effort, given that
  1706. // sanitizing is only needed for misformatted metadata anyway, but
  1707. // this at least covers the case when $arr is numeric.
  1708. $sanitizedKey .= $counter;
  1709. ++$counter;
  1710. }
  1711. $arr[$sanitizedKey] = $arr[$key];
  1712. unset( $arr[$key] );
  1713. }
  1714. if ( is_array( $value ) ) {
  1715. $this->sanitizeArrayForAPI( $value );
  1716. }
  1717. }
  1718. // Handle API metadata keys (particularly "_type")
  1719. $keys = array_filter( array_keys( $arr ), 'ApiResult::isMetadataKey' );
  1720. if ( $keys ) {
  1721. ApiResult::setPreserveKeysList( $arr, $keys );
  1722. }
  1723. }
  1724. /**
  1725. * Turns a string into a valid API identifier.
  1726. * @param string $key
  1727. * @return string
  1728. * @since 1.23
  1729. */
  1730. protected function sanitizeKeyForAPI( $key ) {
  1731. // drop all characters which are not valid in an XML tag name
  1732. // a bunch of non-ASCII letters would be valid but probably won't
  1733. // be used so we take the easy way
  1734. $key = preg_replace( '/[^a-zA-z0-9_:.\-]/', '', $key );
  1735. // drop characters which are invalid at the first position
  1736. $key = preg_replace( '/^[\d\-.]+/', '', $key );
  1737. if ( $key == '' ) {
  1738. $key = '_';
  1739. }
  1740. // special case for an internal keyword
  1741. if ( $key == '_element' ) {
  1742. $key = 'element';
  1743. }
  1744. return $key;
  1745. }
  1746. /**
  1747. * Returns a list of languages (first is best) to use when formatting multilang fields,
  1748. * based on user and site preferences.
  1749. * @return array
  1750. * @since 1.23
  1751. */
  1752. protected function getPriorityLanguages() {
  1753. $priorityLanguages =
  1754. Language::getFallbacksIncludingSiteLanguage( $this->getLanguage()->getCode() );
  1755. $priorityLanguages = array_merge(
  1756. (array)$this->getLanguage()->getCode(),
  1757. $priorityLanguages[0],
  1758. $priorityLanguages[1]
  1759. );
  1760. return $priorityLanguages;
  1761. }
  1762. }