Fixed dependencies for jquery.collapsibleTabs
[lhc/web/wiklou.git] / includes / media / FormatMetadata.php
1 <?php
2 /**
3 * Formating 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 http://www.gnu.org/copyleft/gpl.html GNU General Public License
24 * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
25 * @file
26 */
27
28
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 */
48 class FormatMetadata {
49
50 /**
51 * Numbers given by Exif user agents are often magical, that is they
52 * should be replaced by a detailed explanation depending on their
53 * value which most of the time are plain integers. This function
54 * formats Exif (and other metadata) values into human readable form.
55 *
56 * @param $tags Array: the Exif data to format ( as returned by
57 * Exif::getFilteredData() or BitmapMetadataHandler )
58 * @return array
59 */
60 public static function getFormattedData( $tags ) {
61 global $wgLang;
62
63 $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
64 unset( $tags['ResolutionUnit'] );
65
66 foreach ( $tags as $tag => &$vals ) {
67
68 // This seems ugly to wrap non-array's in an array just to unwrap again,
69 // especially when most of the time it is not an array
70 if ( !is_array( $tags[$tag] ) ) {
71 $vals = Array( $vals );
72 }
73
74 // _type is a special value to say what array type
75 if ( isset( $tags[$tag]['_type'] ) ) {
76 $type = $tags[$tag]['_type'];
77 unset( $vals['_type'] );
78 } else {
79 $type = 'ul'; // default unordered list.
80 }
81
82 //This is done differently as the tag is an array.
83 if ($tag == 'GPSTimeStamp' && count($vals) === 3) {
84 //hour min sec array
85
86 $h = explode('/', $vals[0]);
87 $m = explode('/', $vals[1]);
88 $s = explode('/', $vals[2]);
89
90 // this should already be validated
91 // when loaded from file, but it could
92 // come from a foreign repo, so be
93 // paranoid.
94 if ( !isset($h[1])
95 || !isset($m[1])
96 || !isset($s[1])
97 || $h[1] == 0
98 || $m[1] == 0
99 || $s[1] == 0
100 ) {
101 continue;
102 }
103 $tags[$tag] = str_pad( intval( $h[0] / $h[1] ), 2, '0', STR_PAD_LEFT )
104 . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT )
105 . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT );
106
107 try {
108 $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] );
109 // the 1971:01:01 is just a placeholder, and not shown to user.
110 if ( $time && intval( $time ) > 0 ) {
111 $tags[$tag] = $wgLang->time( $time );
112 }
113 } catch ( TimestampException $e ) {
114 // This shouldn't happen, but we've seen bad formats
115 // such as 4-digit seconds in the wild.
116 // leave $tags[$tag] as-is
117 }
118 continue;
119 }
120
121 // The contact info is a multi-valued field
122 // instead of the other props which are single
123 // valued (mostly) so handle as a special case.
124 if ( $tag === 'Contact' ) {
125 $vals = self::collapseContactInfo( $vals );
126 continue;
127 }
128
129 foreach ( $vals as &$val ) {
130
131 switch( $tag ) {
132 case 'Compression':
133 switch( $val ) {
134 case 1: case 2: case 3: case 4:
135 case 5: case 6: case 7: case 8:
136 case 32773: case 32946: case 34712:
137 $val = self::msg( $tag, $val );
138 break;
139 default:
140 /* If not recognized, display as is. */
141 break;
142 }
143 break;
144
145 case 'PhotometricInterpretation':
146 switch( $val ) {
147 case 2: case 6:
148 $val = self::msg( $tag, $val );
149 break;
150 default:
151 /* If not recognized, display as is. */
152 break;
153 }
154 break;
155
156 case 'Orientation':
157 switch( $val ) {
158 case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
159 $val = self::msg( $tag, $val );
160 break;
161 default:
162 /* If not recognized, display as is. */
163 break;
164 }
165 break;
166
167 case 'PlanarConfiguration':
168 switch( $val ) {
169 case 1: case 2:
170 $val = self::msg( $tag, $val );
171 break;
172 default:
173 /* If not recognized, display as is. */
174 break;
175 }
176 break;
177
178 // TODO: YCbCrSubSampling
179 case 'YCbCrPositioning':
180 switch ( $val ) {
181 case 1:
182 case 2:
183 $val = self::msg( $tag, $val );
184 break;
185 default:
186 /* If not recognized, display as is. */
187 break;
188 }
189 break;
190
191 case 'XResolution':
192 case 'YResolution':
193 switch( $resolutionunit ) {
194 case 2:
195 $val = self::msg( 'XYResolution', 'i', self::formatNum( $val ) );
196 break;
197 case 3:
198 $val = self::msg( 'XYResolution', 'c', self::formatNum( $val ) );
199 break;
200 default:
201 /* If not recognized, display as is. */
202 break;
203 }
204 break;
205
206 // TODO: YCbCrCoefficients #p27 (see annex E)
207 case 'ExifVersion': case 'FlashpixVersion':
208 $val = "$val" / 100;
209 break;
210
211 case 'ColorSpace':
212 switch( $val ) {
213 case 1: case 65535:
214 $val = self::msg( $tag, $val );
215 break;
216 default:
217 /* If not recognized, display as is. */
218 break;
219 }
220 break;
221
222 case 'ComponentsConfiguration':
223 switch( $val ) {
224 case 0: case 1: case 2: case 3: case 4: case 5: case 6:
225 $val = self::msg( $tag, $val );
226 break;
227 default:
228 /* If not recognized, display as is. */
229 break;
230 }
231 break;
232
233 case 'DateTime':
234 case 'DateTimeOriginal':
235 case 'DateTimeDigitized':
236 case 'DateTimeReleased':
237 case 'DateTimeExpires':
238 case 'GPSDateStamp':
239 case 'dc-date':
240 case 'DateTimeMetadata':
241 if ( $val == '0000:00:00 00:00:00' || $val == ' : : : : ' ) {
242 $val = wfMessage( 'exif-unknowndate' )->text();
243 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) {
244 // Full date.
245 $time = wfTimestamp( TS_MW, $val );
246 if ( $time && intval( $time ) > 0 ) {
247 $val = $wgLang->timeanddate( $time );
248 }
249 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) {
250 // No second field. Still format the same
251 // since timeanddate doesn't include seconds anyways,
252 // but second still available in api
253 $time = wfTimestamp( TS_MW, $val . ':00' );
254 if ( $time && intval( $time ) > 0 ) {
255 $val = $wgLang->timeanddate( $time );
256 }
257 } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
258 // If only the date but not the time is filled in.
259 $time = wfTimestamp( TS_MW, substr( $val, 0, 4 )
260 . substr( $val, 5, 2 )
261 . substr( $val, 8, 2 )
262 . '000000' );
263 if ( $time && intval( $time ) > 0 ) {
264 $val = $wgLang->date( $time );
265 }
266 }
267 // else it will just output $val without formatting it.
268 break;
269
270 case 'ExposureProgram':
271 switch( $val ) {
272 case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
273 $val = self::msg( $tag, $val );
274 break;
275 default:
276 /* If not recognized, display as is. */
277 break;
278 }
279 break;
280
281 case 'SubjectDistance':
282 $val = self::msg( $tag, '', self::formatNum( $val ) );
283 break;
284
285 case 'MeteringMode':
286 switch( $val ) {
287 case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255:
288 $val = self::msg( $tag, $val );
289 break;
290 default:
291 /* If not recognized, display as is. */
292 break;
293 }
294 break;
295
296 case 'LightSource':
297 switch( $val ) {
298 case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11:
299 case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20:
300 case 21: case 22: case 23: case 24: case 255:
301 $val = self::msg( $tag, $val );
302 break;
303 default:
304 /* If not recognized, display as is. */
305 break;
306 }
307 break;
308
309 case 'Flash':
310 $flashDecode = array(
311 'fired' => $val & bindec( '00000001' ),
312 'return' => ( $val & bindec( '00000110' ) ) >> 1,
313 'mode' => ( $val & bindec( '00011000' ) ) >> 3,
314 'function' => ( $val & bindec( '00100000' ) ) >> 5,
315 'redeye' => ( $val & bindec( '01000000' ) ) >> 6,
316 // 'reserved' => ($val & bindec( '10000000' )) >> 7,
317 );
318 $flashMsgs = array();
319 # We do not need to handle unknown values since all are used.
320 foreach ( $flashDecode as $subTag => $subValue ) {
321 # We do not need any message for zeroed values.
322 if ( $subTag != 'fired' && $subValue == 0 ) {
323 continue;
324 }
325 $fullTag = $tag . '-' . $subTag ;
326 $flashMsgs[] = self::msg( $fullTag, $subValue );
327 }
328 $val = $wgLang->commaList( $flashMsgs );
329 break;
330
331 case 'FocalPlaneResolutionUnit':
332 switch( $val ) {
333 case 2:
334 $val = self::msg( $tag, $val );
335 break;
336 default:
337 /* If not recognized, display as is. */
338 break;
339 }
340 break;
341
342 case 'SensingMethod':
343 switch( $val ) {
344 case 1: case 2: case 3: case 4: case 5: case 7: case 8:
345 $val = self::msg( $tag, $val );
346 break;
347 default:
348 /* If not recognized, display as is. */
349 break;
350 }
351 break;
352
353 case 'FileSource':
354 switch( $val ) {
355 case 3:
356 $val = self::msg( $tag, $val );
357 break;
358 default:
359 /* If not recognized, display as is. */
360 break;
361 }
362 break;
363
364 case 'SceneType':
365 switch( $val ) {
366 case 1:
367 $val = self::msg( $tag, $val );
368 break;
369 default:
370 /* If not recognized, display as is. */
371 break;
372 }
373 break;
374
375 case 'CustomRendered':
376 switch( $val ) {
377 case 0: case 1:
378 $val = self::msg( $tag, $val );
379 break;
380 default:
381 /* If not recognized, display as is. */
382 break;
383 }
384 break;
385
386 case 'ExposureMode':
387 switch( $val ) {
388 case 0: case 1: case 2:
389 $val = self::msg( $tag, $val );
390 break;
391 default:
392 /* If not recognized, display as is. */
393 break;
394 }
395 break;
396
397 case 'WhiteBalance':
398 switch( $val ) {
399 case 0: case 1:
400 $val = self::msg( $tag, $val );
401 break;
402 default:
403 /* If not recognized, display as is. */
404 break;
405 }
406 break;
407
408 case 'SceneCaptureType':
409 switch( $val ) {
410 case 0: case 1: case 2: case 3:
411 $val = self::msg( $tag, $val );
412 break;
413 default:
414 /* If not recognized, display as is. */
415 break;
416 }
417 break;
418
419 case 'GainControl':
420 switch( $val ) {
421 case 0: case 1: case 2: case 3: case 4:
422 $val = self::msg( $tag, $val );
423 break;
424 default:
425 /* If not recognized, display as is. */
426 break;
427 }
428 break;
429
430 case 'Contrast':
431 switch( $val ) {
432 case 0: case 1: case 2:
433 $val = self::msg( $tag, $val );
434 break;
435 default:
436 /* If not recognized, display as is. */
437 break;
438 }
439 break;
440
441 case 'Saturation':
442 switch( $val ) {
443 case 0: case 1: case 2:
444 $val = self::msg( $tag, $val );
445 break;
446 default:
447 /* If not recognized, display as is. */
448 break;
449 }
450 break;
451
452 case 'Sharpness':
453 switch( $val ) {
454 case 0: case 1: case 2:
455 $val = self::msg( $tag, $val );
456 break;
457 default:
458 /* If not recognized, display as is. */
459 break;
460 }
461 break;
462
463 case 'SubjectDistanceRange':
464 switch( $val ) {
465 case 0: case 1: case 2: case 3:
466 $val = self::msg( $tag, $val );
467 break;
468 default:
469 /* If not recognized, display as is. */
470 break;
471 }
472 break;
473
474 //The GPS...Ref values are kept for compatibility, probably won't be reached.
475 case 'GPSLatitudeRef':
476 case 'GPSDestLatitudeRef':
477 switch( $val ) {
478 case 'N': case 'S':
479 $val = self::msg( 'GPSLatitude', $val );
480 break;
481 default:
482 /* If not recognized, display as is. */
483 break;
484 }
485 break;
486
487 case 'GPSLongitudeRef':
488 case 'GPSDestLongitudeRef':
489 switch( $val ) {
490 case 'E': case 'W':
491 $val = self::msg( 'GPSLongitude', $val );
492 break;
493 default:
494 /* If not recognized, display as is. */
495 break;
496 }
497 break;
498
499 case 'GPSAltitude':
500 if ( $val < 0 ) {
501 $val = self::msg( 'GPSAltitude', 'below-sealevel', self::formatNum( -$val, 3 ) );
502 } else {
503 $val = self::msg( 'GPSAltitude', 'above-sealevel', self::formatNum( $val, 3 ) );
504 }
505 break;
506
507 case 'GPSStatus':
508 switch( $val ) {
509 case 'A': case 'V':
510 $val = self::msg( $tag, $val );
511 break;
512 default:
513 /* If not recognized, display as is. */
514 break;
515 }
516 break;
517
518 case 'GPSMeasureMode':
519 switch( $val ) {
520 case 2: case 3:
521 $val = self::msg( $tag, $val );
522 break;
523 default:
524 /* If not recognized, display as is. */
525 break;
526 }
527 break;
528
529
530 case 'GPSTrackRef':
531 case 'GPSImgDirectionRef':
532 case 'GPSDestBearingRef':
533 switch( $val ) {
534 case 'T': case 'M':
535 $val = self::msg( 'GPSDirection', $val );
536 break;
537 default:
538 /* If not recognized, display as is. */
539 break;
540 }
541 break;
542
543 case 'GPSLatitude':
544 case 'GPSDestLatitude':
545 $val = self::formatCoords( $val, 'latitude' );
546 break;
547 case 'GPSLongitude':
548 case 'GPSDestLongitude':
549 $val = self::formatCoords( $val, 'longitude' );
550 break;
551
552 case 'GPSSpeedRef':
553 switch( $val ) {
554 case 'K': case 'M': case 'N':
555 $val = self::msg( 'GPSSpeed', $val );
556 break;
557 default:
558 /* If not recognized, display as is. */
559 break;
560 }
561 break;
562
563 case 'GPSDestDistanceRef':
564 switch( $val ) {
565 case 'K': case 'M': case 'N':
566 $val = self::msg( 'GPSDestDistance', $val );
567 break;
568 default:
569 /* If not recognized, display as is. */
570 break;
571 }
572 break;
573
574 case 'GPSDOP':
575 // See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)
576 if ( $val <= 2 ) {
577 $val = self::msg( $tag, 'excellent', self::formatNum( $val ) );
578 } elseif ( $val <= 5 ) {
579 $val = self::msg( $tag, 'good', self::formatNum( $val ) );
580 } elseif ( $val <= 10 ) {
581 $val = self::msg( $tag, 'moderate', self::formatNum( $val ) );
582 } elseif ( $val <= 20 ) {
583 $val = self::msg( $tag, 'fair', self::formatNum( $val ) );
584 } else {
585 $val = self::msg( $tag, 'poor', self::formatNum( $val ) );
586 }
587 break;
588
589 // This is not in the Exif standard, just a special
590 // case for our purposes which enables wikis to wikify
591 // the make, model and software name to link to their articles.
592 case 'Make':
593 case 'Model':
594 $val = self::msg( $tag, '', $val );
595 break;
596
597 case 'Software':
598 if ( is_array( $val ) ) {
599 //if its a software, version array.
600 $val = wfMessage( 'exif-software-version-value', $val[0], $val[1] )->text();
601 } else {
602 $val = self::msg( $tag, '', $val );
603 }
604 break;
605
606 case 'ExposureTime':
607 // Show the pretty fraction as well as decimal version
608 $val = wfMessage( 'exif-exposuretime-format',
609 self::formatFraction( $val ), self::formatNum( $val ) )->text();
610 break;
611 case 'ISOSpeedRatings':
612 // If its = 65535 that means its at the
613 // limit of the size of Exif::short and
614 // is really higher.
615 if ( $val == '65535' ) {
616 $val = self::msg( $tag, 'overflow' );
617 } else {
618 $val = self::formatNum( $val );
619 }
620 break;
621 case 'FNumber':
622 $val = wfMessage( 'exif-fnumber-format',
623 self::formatNum( $val ) )->text();
624 break;
625
626 case 'FocalLength': case 'FocalLengthIn35mmFilm':
627 $val = wfMessage( 'exif-focallength-format',
628 self::formatNum( $val ) )->text();
629 break;
630
631 case 'MaxApertureValue':
632 if ( strpos( $val, '/' ) !== false ) {
633 // need to expand this earlier to calculate fNumber
634 list($n, $d) = explode('/', $val);
635 if ( is_numeric( $n ) && is_numeric( $d ) ) {
636 $val = $n / $d;
637 }
638 }
639 if ( is_numeric( $val ) ) {
640 $fNumber = pow( 2, $val / 2 );
641 if ( $fNumber !== false ) {
642 $val = wfMessage( 'exif-maxaperturevalue-value',
643 self::formatNum( $val ),
644 self::formatNum( $fNumber, 2 )
645 )->text();
646 }
647 }
648 break;
649
650 case 'iimCategory':
651 switch( strtolower( $val ) ) {
652 // See pg 29 of IPTC photo
653 // metadata standard.
654 case 'ace': case 'clj':
655 case 'dis': case 'fin':
656 case 'edu': case 'evn':
657 case 'hth': case 'hum':
658 case 'lab': case 'lif':
659 case 'pol': case 'rel':
660 case 'sci': case 'soi':
661 case 'spo': case 'war':
662 case 'wea':
663 $val = self::msg(
664 'iimcategory',
665 $val
666 );
667 }
668 break;
669 case 'SubjectNewsCode':
670 // Essentially like iimCategory.
671 // 8 (numeric) digit hierarchical
672 // classification. We decode the
673 // first 2 digits, which provide
674 // a broad category.
675 $val = self::convertNewsCode( $val );
676 break;
677 case 'Urgency':
678 // 1-8 with 1 being highest, 5 normal
679 // 0 is reserved, and 9 is 'user-defined'.
680 $urgency = '';
681 if ( $val == 0 || $val == 9 ) {
682 $urgency = 'other';
683 } elseif ( $val < 5 && $val > 1 ) {
684 $urgency = 'high';
685 } elseif ( $val == 5 ) {
686 $urgency = 'normal';
687 } elseif ( $val <= 8 && $val > 5) {
688 $urgency = 'low';
689 }
690
691 if ( $urgency !== '' ) {
692 $val = self::msg( 'urgency',
693 $urgency, $val
694 );
695 }
696 break;
697
698 // Things that have a unit of pixels.
699 case 'OriginalImageHeight':
700 case 'OriginalImageWidth':
701 case 'PixelXDimension':
702 case 'PixelYDimension':
703 case 'ImageWidth':
704 case 'ImageLength':
705 $val = self::formatNum( $val ) . ' ' . wfMessage( 'unit-pixel' )->text();
706 break;
707
708 // Do not transform fields with pure text.
709 // For some languages the formatNum()
710 // conversion results to wrong output like
711 // foo,bar@example,com or foo٫bar@example٫com.
712 // Also some 'numeric' things like Scene codes
713 // are included here as we really don't want
714 // commas inserted.
715 case 'ImageDescription':
716 case 'Artist':
717 case 'Copyright':
718 case 'RelatedSoundFile':
719 case 'ImageUniqueID':
720 case 'SpectralSensitivity':
721 case 'GPSSatellites':
722 case 'GPSVersionID':
723 case 'GPSMapDatum':
724 case 'Keywords':
725 case 'WorldRegionDest':
726 case 'CountryDest':
727 case 'CountryCodeDest':
728 case 'ProvinceOrStateDest':
729 case 'CityDest':
730 case 'SublocationDest':
731 case 'WorldRegionCreated':
732 case 'CountryCreated':
733 case 'CountryCodeCreated':
734 case 'ProvinceOrStateCreated':
735 case 'CityCreated':
736 case 'SublocationCreated':
737 case 'ObjectName':
738 case 'SpecialInstructions':
739 case 'Headline':
740 case 'Credit':
741 case 'Source':
742 case 'EditStatus':
743 case 'FixtureIdentifier':
744 case 'LocationDest':
745 case 'LocationDestCode':
746 case 'Writer':
747 case 'JPEGFileComment':
748 case 'iimSupplementalCategory':
749 case 'OriginalTransmissionRef':
750 case 'Identifier':
751 case 'dc-contributor':
752 case 'dc-coverage':
753 case 'dc-publisher':
754 case 'dc-relation':
755 case 'dc-rights':
756 case 'dc-source':
757 case 'dc-type':
758 case 'Lens':
759 case 'SerialNumber':
760 case 'CameraOwnerName':
761 case 'Label':
762 case 'Nickname':
763 case 'RightsCertificate':
764 case 'CopyrightOwner':
765 case 'UsageTerms':
766 case 'WebStatement':
767 case 'OriginalDocumentID':
768 case 'LicenseUrl':
769 case 'MorePermissionsUrl':
770 case 'AttributionUrl':
771 case 'PreferredAttributionName':
772 case 'PNGFileComment':
773 case 'Disclaimer':
774 case 'ContentWarning':
775 case 'GIFFileComment':
776 case 'SceneCode':
777 case 'IntellectualGenre':
778 case 'Event':
779 case 'OrginisationInImage':
780 case 'PersonInImage':
781
782 $val = htmlspecialchars( $val );
783 break;
784
785 case 'ObjectCycle':
786 switch ( $val ) {
787 case 'a': case 'p': case 'b':
788 $val = self::msg( $tag, $val );
789 break;
790 default:
791 $val = htmlspecialchars( $val );
792 break;
793 }
794 break;
795 case 'Copyrighted':
796 switch( $val ) {
797 case 'True': case 'False':
798 $val = self::msg( $tag, $val );
799 break;
800 }
801 break;
802 case 'Rating':
803 if ( $val == '-1' ) {
804 $val = self::msg( $tag, 'rejected' );
805 } else {
806 $val = self::formatNum( $val );
807 }
808 break;
809
810 case 'LanguageCode':
811 $lang = Language::fetchLanguageName( strtolower( $val ), $wgLang->getCode() );
812 if ($lang) {
813 $val = htmlspecialchars( $lang );
814 } else {
815 $val = htmlspecialchars( $val );
816 }
817 break;
818
819 default:
820 $val = self::formatNum( $val );
821 break;
822 }
823 }
824 // End formatting values, start flattening arrays.
825 $vals = self::flattenArray( $vals, $type );
826
827 }
828 return $tags;
829 }
830
831 /**
832 * A function to collapse multivalued tags into a single value.
833 * This turns an array of (for example) authors into a bulleted list.
834 *
835 * This is public on the basis it might be useful outside of this class.
836 *
837 * @param $vals Array array of values
838 * @param $type String Type of array (either lang, ul, ol).
839 * lang = language assoc array with keys being the lang code
840 * ul = unordered list, ol = ordered list
841 * type can also come from the '_type' member of $vals.
842 * @param $noHtml Boolean If to avoid returning anything resembling
843 * html. (Ugly hack for backwards compatibility with old mediawiki).
844 * @return String single value (in wiki-syntax).
845 */
846 public static function flattenArray( $vals, $type = 'ul', $noHtml = false ) {
847 if ( isset( $vals['_type'] ) ) {
848 $type = $vals['_type'];
849 unset( $vals['_type'] );
850 }
851
852 if ( !is_array( $vals ) ) {
853 return $vals; // do nothing if not an array;
854 }
855 elseif ( count( $vals ) === 1 && $type !== 'lang' ) {
856 return $vals[0];
857 }
858 elseif ( count( $vals ) === 0 ) {
859 wfDebug( __METHOD__ . ' metadata array with 0 elements!' );
860 return ""; // paranoia. This should never happen
861 }
862 /* @todo FIXME: This should hide some of the list entries if there are
863 * say more than four. Especially if a field is translated into 20
864 * languages, we don't want to show them all by default
865 */
866 else {
867 global $wgContLang;
868 switch( $type ) {
869 case 'lang':
870 // Display default, followed by ContLang,
871 // followed by the rest in no particular
872 // order.
873
874 // Todo: hide some items if really long list.
875
876 $content = '';
877
878 $cLang = $wgContLang->getCode();
879 $defaultItem = false;
880 $defaultLang = false;
881
882 // If default is set, save it for later,
883 // as we don't know if it's equal to
884 // one of the lang codes. (In xmp
885 // you specify the language for a
886 // default property by having both
887 // a default prop, and one in the language
888 // that are identical)
889 if ( isset( $vals['x-default'] ) ) {
890 $defaultItem = $vals['x-default'];
891 unset( $vals['x-default'] );
892 }
893 // Do contentLanguage.
894 if ( isset( $vals[$cLang] ) ) {
895 $isDefault = false;
896 if ( $vals[$cLang] === $defaultItem ) {
897 $defaultItem = false;
898 $isDefault = true;
899 }
900 $content .= self::langItem(
901 $vals[$cLang], $cLang,
902 $isDefault, $noHtml );
903
904 unset( $vals[$cLang] );
905 }
906
907 // Now do the rest.
908 foreach ( $vals as $lang => $item ) {
909 if ( $item === $defaultItem ) {
910 $defaultLang = $lang;
911 continue;
912 }
913 $content .= self::langItem( $item,
914 $lang, false, $noHtml );
915 }
916 if ( $defaultItem !== false ) {
917 $content = self::langItem( $defaultItem,
918 $defaultLang, true, $noHtml )
919 . $content;
920 }
921 if ( $noHtml ) {
922 return $content;
923 }
924 return '<ul class="metadata-langlist">' .
925 $content .
926 '</ul>';
927 case 'ol':
928 if ( $noHtml ) {
929 return "\n#" . implode( "\n#", $vals );
930 }
931 return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>';
932 case 'ul':
933 default:
934 if ( $noHtml ) {
935 return "\n*" . implode( "\n*", $vals );
936 }
937 return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>';
938 }
939 }
940 }
941
942 /** Helper function for creating lists of translations.
943 *
944 * @param $value String value (this is not escaped)
945 * @param $lang String lang code of item or false
946 * @param $default Boolean if it is default value.
947 * @param $noHtml Boolean If to avoid html (for back-compat)
948 * @throws MWException
949 * @return string language item (Note: despite how this looks,
950 * this is treated as wikitext not html).
951 */
952 private static function langItem( $value, $lang, $default = false, $noHtml = false ) {
953 if ( $lang === false && $default === false) {
954 throw new MWException('$lang and $default cannot both '
955 . 'be false.');
956 }
957
958 if ( $noHtml ) {
959 $wrappedValue = $value;
960 } else {
961 $wrappedValue = '<span class="mw-metadata-lang-value">'
962 . $value . '</span>';
963 }
964
965 if ( $lang === false ) {
966 if ( $noHtml ) {
967 return wfMessage( 'metadata-langitem-default',
968 $wrappedValue )->text() . "\n\n";
969 } /* else */
970 return '<li class="mw-metadata-lang-default">'
971 . wfMessage( 'metadata-langitem-default',
972 $wrappedValue )->text()
973 . "</li>\n";
974 }
975
976 $lowLang = strtolower( $lang );
977 $langName = Language::fetchLanguageName( $lowLang );
978 if ( $langName === '' ) {
979 //try just the base language name. (aka en-US -> en ).
980 list( $langPrefix ) = explode( '-', $lowLang, 2 );
981 $langName = Language::fetchLanguageName( $langPrefix );
982 if ( $langName === '' ) {
983 // give up.
984 $langName = $lang;
985 }
986 }
987 // else we have a language specified
988
989 if ( $noHtml ) {
990 return '*' . wfMessage( 'metadata-langitem',
991 $wrappedValue, $langName, $lang )->text();
992 } /* else: */
993
994 $item = '<li class="mw-metadata-lang-code-'
995 . $lang;
996 if ( $default ) {
997 $item .= ' mw-metadata-lang-default';
998 }
999 $item .= '" lang="' . $lang . '">';
1000 $item .= wfMessage( 'metadata-langitem',
1001 $wrappedValue, $langName, $lang )->text();
1002 $item .= "</li>\n";
1003 return $item;
1004 }
1005
1006 /**
1007 * Convenience function for getFormattedData()
1008 *
1009 * @private
1010 *
1011 * @param $tag String: the tag name to pass on
1012 * @param $val String: the value of the tag
1013 * @param $arg String: an argument to pass ($1)
1014 * @param $arg2 String: a 2nd argument to pass ($2)
1015 * @return string A wfMessage of "exif-$tag-$val" in lower case
1016 */
1017 static function msg( $tag, $val, $arg = null, $arg2 = null ) {
1018 global $wgContLang;
1019
1020 if ($val === '')
1021 $val = 'value';
1022 return wfMessage( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text();
1023 }
1024
1025 /**
1026 * Format a number, convert numbers from fractions into floating point
1027 * numbers, joins arrays of numbers with commas.
1028 *
1029 * @param $num Mixed: the value to format
1030 * @param $round float|int|bool digits to round to or false.
1031 * @return mixed A floating point number or whatever we were fed
1032 */
1033 static function formatNum( $num, $round = false ) {
1034 global $wgLang;
1035 $m = array();
1036 if( is_array($num) ) {
1037 $out = array();
1038 foreach( $num as $number ) {
1039 $out[] = self::formatNum($number);
1040 }
1041 return $wgLang->commaList( $out );
1042 }
1043 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1044 if ( $m[2] != 0 ) {
1045 $newNum = $m[1] / $m[2];
1046 if ( $round !== false ) {
1047 $newNum = round( $newNum, $round );
1048 }
1049 } else {
1050 $newNum = $num;
1051 }
1052
1053 return $wgLang->formatNum( $newNum );
1054 } else {
1055 if ( is_numeric( $num ) && $round !== false ) {
1056 $num = round( $num, $round );
1057 }
1058 return $wgLang->formatNum( $num );
1059 }
1060 }
1061
1062 /**
1063 * Format a rational number, reducing fractions
1064 *
1065 * @private
1066 *
1067 * @param $num Mixed: the value to format
1068 * @return mixed A floating point number or whatever we were fed
1069 */
1070 static function formatFraction( $num ) {
1071 $m = array();
1072 if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
1073 $numerator = intval( $m[1] );
1074 $denominator = intval( $m[2] );
1075 $gcd = self::gcd( abs( $numerator ), $denominator );
1076 if( $gcd != 0 ) {
1077 // 0 shouldn't happen! ;)
1078 return self::formatNum( $numerator / $gcd ) . '/' . self::formatNum( $denominator / $gcd );
1079 }
1080 }
1081 return self::formatNum( $num );
1082 }
1083
1084 /**
1085 * Calculate the greatest common divisor of two integers.
1086 *
1087 * @param $a Integer: Numerator
1088 * @param $b Integer: Denominator
1089 * @return int
1090 * @private
1091 */
1092 static function gcd( $a, $b ) {
1093 /*
1094 // http://en.wikipedia.org/wiki/Euclidean_algorithm
1095 // Recursive form would be:
1096 if( $b == 0 )
1097 return $a;
1098 else
1099 return gcd( $b, $a % $b );
1100 */
1101 while( $b != 0 ) {
1102 $remainder = $a % $b;
1103
1104 // tail recursion...
1105 $a = $b;
1106 $b = $remainder;
1107 }
1108 return $a;
1109 }
1110
1111 /**
1112 * Fetch the human readable version of a news code.
1113 * A news code is an 8 digit code. The first two
1114 * digits are a general classification, so we just
1115 * translate that.
1116 *
1117 * Note, leading 0's are significant, so this is
1118 * a string, not an int.
1119 *
1120 * @param $val String: The 8 digit news code.
1121 * @return string The human readable form
1122 */
1123 static private function convertNewsCode( $val ) {
1124 if ( !preg_match( '/^\d{8}$/D', $val ) ) {
1125 // Not a valid news code.
1126 return $val;
1127 }
1128 $cat = '';
1129 switch( substr( $val , 0, 2 ) ) {
1130 case '01':
1131 $cat = 'ace';
1132 break;
1133 case '02':
1134 $cat = 'clj';
1135 break;
1136 case '03':
1137 $cat = 'dis';
1138 break;
1139 case '04':
1140 $cat = 'fin';
1141 break;
1142 case '05':
1143 $cat = 'edu';
1144 break;
1145 case '06':
1146 $cat = 'evn';
1147 break;
1148 case '07':
1149 $cat = 'hth';
1150 break;
1151 case '08':
1152 $cat = 'hum';
1153 break;
1154 case '09':
1155 $cat = 'lab';
1156 break;
1157 case '10':
1158 $cat = 'lif';
1159 break;
1160 case '11':
1161 $cat = 'pol';
1162 break;
1163 case '12':
1164 $cat = 'rel';
1165 break;
1166 case '13':
1167 $cat = 'sci';
1168 break;
1169 case '14':
1170 $cat = 'soi';
1171 break;
1172 case '15':
1173 $cat = 'spo';
1174 break;
1175 case '16':
1176 $cat = 'war';
1177 break;
1178 case '17':
1179 $cat = 'wea';
1180 break;
1181 }
1182 if ( $cat !== '' ) {
1183 $catMsg = self::msg( 'iimcategory', $cat );
1184 $val = self::msg( 'subjectnewscode', '', $val, $catMsg );
1185 }
1186 return $val;
1187 }
1188
1189 /**
1190 * Format a coordinate value, convert numbers from floating point
1191 * into degree minute second representation.
1192 *
1193 * @param $coord int degrees, minutes and seconds
1194 * @param $type String: latitude or longitude (for if its a NWS or E)
1195 * @return mixed A floating point number or whatever we were fed
1196 */
1197 static function formatCoords( $coord, $type ) {
1198 $ref = '';
1199 if ( $coord < 0 ) {
1200 $nCoord = -$coord;
1201 if ( $type === 'latitude' ) {
1202 $ref = 'S';
1203 } elseif ( $type === 'longitude' ) {
1204 $ref = 'W';
1205 }
1206 } else {
1207 $nCoord = $coord;
1208 if ( $type === 'latitude' ) {
1209 $ref = 'N';
1210 } elseif ( $type === 'longitude' ) {
1211 $ref = 'E';
1212 }
1213 }
1214
1215 $deg = floor( $nCoord );
1216 $min = floor( ( $nCoord - $deg ) * 60.0 );
1217 $sec = round( ( ( $nCoord - $deg ) - $min / 60 ) * 3600, 2 );
1218
1219 $deg = self::formatNum( $deg );
1220 $min = self::formatNum( $min );
1221 $sec = self::formatNum( $sec );
1222
1223 return wfMessage( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text();
1224 }
1225
1226 /**
1227 * Format the contact info field into a single value.
1228 *
1229 * @param $vals Array array with fields of the ContactInfo
1230 * struct defined in the IPTC4XMP spec. Or potentially
1231 * an array with one element that is a free form text
1232 * value from the older iptc iim 1:118 prop.
1233 *
1234 * This function might be called from
1235 * JpegHandler::convertMetadataVersion which is why it is
1236 * public.
1237 *
1238 * @return String of html-ish looking wikitext
1239 */
1240 public static function collapseContactInfo( $vals ) {
1241 if( ! ( isset( $vals['CiAdrExtadr'] )
1242 || isset( $vals['CiAdrCity'] )
1243 || isset( $vals['CiAdrCtry'] )
1244 || isset( $vals['CiEmailWork'] )
1245 || isset( $vals['CiTelWork'] )
1246 || isset( $vals['CiAdrPcode'] )
1247 || isset( $vals['CiAdrRegion'] )
1248 || isset( $vals['CiUrlWork'] )
1249 ) ) {
1250 // We don't have any sub-properties
1251 // This could happen if its using old
1252 // iptc that just had this as a free-form
1253 // text value.
1254 // Note: We run this through htmlspecialchars
1255 // partially to be consistent, and partially
1256 // because people often insert >, etc into
1257 // the metadata which should not be interpreted
1258 // but we still want to auto-link urls.
1259 foreach( $vals as &$val ) {
1260 $val = htmlspecialchars( $val );
1261 }
1262 return self::flattenArray( $vals );
1263 } else {
1264 // We have a real ContactInfo field.
1265 // Its unclear if all these fields have to be
1266 // set, so assume they do not.
1267 $url = $tel = $street = $city = $country = '';
1268 $email = $postal = $region = '';
1269
1270 // Also note, some of the class names this uses
1271 // are similar to those used by hCard. This is
1272 // mostly because they're sensible names. This
1273 // does not (and does not attempt to) output
1274 // stuff in the hCard microformat. However it
1275 // might output in the adr microformat.
1276
1277 if ( isset( $vals['CiAdrExtadr'] ) ) {
1278 // Todo: This can potentially be multi-line.
1279 // Need to check how that works in XMP.
1280 $street = '<span class="extended-address">'
1281 . htmlspecialchars(
1282 $vals['CiAdrExtadr'] )
1283 . '</span>';
1284 }
1285 if ( isset( $vals['CiAdrCity'] ) ) {
1286 $city = '<span class="locality">'
1287 . htmlspecialchars( $vals['CiAdrCity'] )
1288 . '</span>';
1289 }
1290 if ( isset( $vals['CiAdrCtry'] ) ) {
1291 $country = '<span class="country-name">'
1292 . htmlspecialchars( $vals['CiAdrCtry'] )
1293 . '</span>';
1294 }
1295 if ( isset( $vals['CiEmailWork'] ) ) {
1296 $emails = array();
1297 // Have to split multiple emails at commas/new lines.
1298 $splitEmails = explode( "\n", $vals['CiEmailWork'] );
1299 foreach ( $splitEmails as $e1 ) {
1300 // Also split on comma
1301 foreach ( explode( ',', $e1 ) as $e2 ) {
1302 $finalEmail = trim( $e2 );
1303 if ( $finalEmail == ',' || $finalEmail == '' ) {
1304 continue;
1305 }
1306 if ( strpos( $finalEmail, '<' ) !== false ) {
1307 // Don't do fancy formatting to
1308 // "My name" <foo@bar.com> style stuff
1309 $emails[] = $finalEmail;
1310 } else {
1311 $emails[] = '[mailto:'
1312 . $finalEmail
1313 . ' <span class="email">'
1314 . $finalEmail
1315 . '</span>]';
1316 }
1317 }
1318 }
1319 $email = implode( ', ', $emails );
1320 }
1321 if ( isset( $vals['CiTelWork'] ) ) {
1322 $tel = '<span class="tel">'
1323 . htmlspecialchars( $vals['CiTelWork'] )
1324 . '</span>';
1325 }
1326 if ( isset( $vals['CiAdrPcode'] ) ) {
1327 $postal = '<span class="postal-code">'
1328 . htmlspecialchars(
1329 $vals['CiAdrPcode'] )
1330 . '</span>';
1331 }
1332 if ( isset( $vals['CiAdrRegion'] ) ) {
1333 // Note this is province/state.
1334 $region = '<span class="region">'
1335 . htmlspecialchars(
1336 $vals['CiAdrRegion'] )
1337 . '</span>';
1338 }
1339 if ( isset( $vals['CiUrlWork'] ) ) {
1340 $url = '<span class="url">'
1341 . htmlspecialchars( $vals['CiUrlWork'] )
1342 . '</span>';
1343 }
1344 return wfMessage( 'exif-contact-value', $email, $url,
1345 $street, $city, $region, $postal, $country,
1346 $tel )->text();
1347 }
1348 }
1349 }
1350
1351 /** For compatability with old FormatExif class
1352 * which some extensions use.
1353 *
1354 * @deprecated since 1.18
1355 *
1356 **/
1357 class FormatExif {
1358 var $meta;
1359
1360 /**
1361 * @param $meta array
1362 */
1363 function FormatExif( $meta ) {
1364 wfDeprecated(__METHOD__);
1365 $this->meta = $meta;
1366 }
1367
1368 /**
1369 * @return array
1370 */
1371 function getFormattedData() {
1372 return FormatMetadata::getFormattedData( $this->meta );
1373 }
1374 }