API: Fix list=deletedrevs paging bug pointed out by Splarka on IRC
[lhc/web/wiklou.git] / includes / Exif.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @ingroup Media
19 * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
20 * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason, 2009 Brent Garber
21 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
22 * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
23 * @file
24 */
25
26 /**
27 * @todo document (e.g. one-sentence class-overview description)
28 * @ingroup Media
29 */
30 class Exif {
31
32 const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer.
33 const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
34 const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer.
35 const LONG = 4; //!< A 32-bit (4-byte) unsigned integer.
36 const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
37 const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition
38 const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation),
39 const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
40
41 //@{
42 /* @var array
43 * @private
44 */
45
46 /**
47 * Exif tags grouped by category, the tagname itself is the key and the type
48 * is the value, in the case of more than one possible value type they are
49 * separated by commas.
50 */
51 var $mExifTags;
52
53 /**
54 * The raw Exif data returned by exif_read_data()
55 */
56 var $mRawExifData;
57
58 /**
59 * A Filtered version of $mRawExifData that has been pruned of invalid
60 * tags and tags that contain content they shouldn't contain according
61 * to the Exif specification
62 */
63 var $mFilteredExifData;
64
65 /**
66 * Filtered and formatted Exif data, see FormatExif::getFormattedData()
67 */
68 var $mFormattedExifData;
69
70 //@}
71
72 //@{
73 /* @var string
74 * @private
75 */
76
77 /**
78 * The file being processed
79 */
80 var $file;
81
82 /**
83 * The basename of the file being processed
84 */
85 var $basename;
86
87 /**
88 * The private log to log to, e.g. 'exif'
89 */
90 var $log = false;
91
92 //@}
93
94 /**
95 * Constructor
96 *
97 * @param $file String: filename.
98 */
99 function __construct( $file ) {
100 /**
101 * Page numbers here refer to pages in the EXIF 2.2 standard
102 *
103 * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification
104 */
105 $this->mExifTags = array(
106 # TIFF Rev. 6.0 Attribute Information (p22)
107 'IFD0' => array(
108 # Tags relating to image structure
109 'ImageWidth' => Exif::SHORT.','.Exif::LONG, # Image width
110 'ImageLength' => Exif::SHORT.','.Exif::LONG, # Image height
111 'BitsPerSample' => array( Exif::SHORT, 3 ), # Number of bits per component
112 # "When a primary image is JPEG compressed, this designation is not"
113 # "necessary and is omitted." (p23)
114 'Compression' => Exif::SHORT, # Compression scheme #p23
115 'PhotometricInterpretation' => Exif::SHORT, # Pixel composition #p23
116 'Orientation' => Exif::SHORT, # Orientation of image #p24
117 'SamplesPerPixel' => Exif::SHORT, # Number of components
118 'PlanarConfiguration' => Exif::SHORT, # Image data arrangement #p24
119 'YCbCrSubSampling' => array( Exif::SHORT, 2), # Subsampling ratio of Y to C #p24
120 'YCbCrPositioning' => Exif::SHORT, # Y and C positioning #p24-25
121 'XResolution' => Exif::RATIONAL, # Image resolution in width direction
122 'YResolution' => Exif::RATIONAL, # Image resolution in height direction
123 'ResolutionUnit' => Exif::SHORT, # Unit of X and Y resolution #(p26)
124
125 # Tags relating to recording offset
126 'StripOffsets' => Exif::SHORT.','.Exif::LONG, # Image data location
127 'RowsPerStrip' => Exif::SHORT.','.Exif::LONG, # Number of rows per strip
128 'StripByteCounts' => Exif::SHORT.','.Exif::LONG, # Bytes per compressed strip
129 'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG, # Offset to JPEG SOI
130 'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG, # Bytes of JPEG data
131
132 # Tags relating to image data characteristics
133 'TransferFunction' => Exif::SHORT, # Transfer function
134 'WhitePoint' => array( Exif::RATIONAL, 2), # White point chromaticity
135 'PrimaryChromaticities' => array( Exif::RATIONAL, 6), # Chromaticities of primarities
136 'YCbCrCoefficients' => array( Exif::RATIONAL, 3), # Color space transformation matrix coefficients #p27
137 'ReferenceBlackWhite' => array( Exif::RATIONAL, 6), # Pair of black and white reference values
138
139 # Other tags
140 'DateTime' => Exif::ASCII, # File change date and time
141 'ImageDescription' => Exif::ASCII, # Image title
142 'Make' => Exif::ASCII, # Image input equipment manufacturer
143 'Model' => Exif::ASCII, # Image input equipment model
144 'Software' => Exif::ASCII, # Software used
145 'Artist' => Exif::ASCII, # Person who created the image
146 'Copyright' => Exif::ASCII, # Copyright holder
147 ),
148
149 # Exif IFD Attribute Information (p30-31)
150 'EXIF' => array(
151 # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance
152 # to the EXIF 2.1 AND 2.2 standards
153 'ExifVersion' => array( Exif::UNDEFINED, 4 ), # Exif version
154 'FlashPixVersion' => array( Exif::UNDEFINED, 4 ), # Supported Flashpix version #p32
155
156 # Tags relating to Image Data Characteristics
157 'ColorSpace' => Exif::SHORT, # Color space information #p32
158
159 # Tags relating to image configuration
160 'ComponentsConfiguration' => array( Exif::UNDEFINED, 1), # Meaning of each component #p33
161 'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode
162 'PixelYDimension' => Exif::SHORT.','.Exif::LONG, # Valid image width
163 'PixelXDimension' => Exif::SHORT.','.Exif::LONG, # Valid image height
164
165 # Tags relating to related user information
166 'MakerNote' => Exif::UNDEFINED, # Manufacturer notes
167 'UserComment' => Exif::UNDEFINED, # User comments #p34
168
169 # Tags relating to related file information
170 'RelatedSoundFile' => Exif::ASCII, # Related audio file
171
172 # Tags relating to date and time
173 'DateTimeOriginal' => Exif::ASCII, # Date and time of original data generation #p36
174 'DateTimeDigitized' => Exif::ASCII, # Date and time of original data generation
175 'SubSecTime' => Exif::ASCII, # DateTime subseconds
176 'SubSecTimeOriginal' => Exif::ASCII, # DateTimeOriginal subseconds
177 'SubSecTimeDigitized' => Exif::ASCII, # DateTimeDigitized subseconds
178
179 # Tags relating to picture-taking conditions (p31)
180 'ExposureTime' => Exif::RATIONAL, # Exposure time
181 'FNumber' => Exif::RATIONAL, # F Number
182 'ExposureProgram' => Exif::SHORT, # Exposure Program #p38
183 'SpectralSensitivity' => Exif::ASCII, # Spectral sensitivity
184 'ISOSpeedRatings' => Exif::SHORT, # ISO speed rating
185 'OECF' => Exif::UNDEFINED, # Optoelectronic conversion factor
186 'ShutterSpeedValue' => Exif::SRATIONAL, # Shutter speed
187 'ApertureValue' => Exif::RATIONAL, # Aperture
188 'BrightnessValue' => Exif::SRATIONAL, # Brightness
189 'ExposureBiasValue' => Exif::SRATIONAL, # Exposure bias
190 'MaxApertureValue' => Exif::RATIONAL, # Maximum land aperture
191 'SubjectDistance' => Exif::RATIONAL, # Subject distance
192 'MeteringMode' => Exif::SHORT, # Metering mode #p40
193 'LightSource' => Exif::SHORT, # Light source #p40-41
194 'Flash' => Exif::SHORT, # Flash #p41-42
195 'FocalLength' => Exif::RATIONAL, # Lens focal length
196 'SubjectArea' => array( Exif::SHORT, 4 ), # Subject area
197 'FlashEnergy' => Exif::RATIONAL, # Flash energy
198 'SpatialFrequencyResponse' => Exif::UNDEFINED, # Spatial frequency response
199 'FocalPlaneXResolution' => Exif::RATIONAL, # Focal plane X resolution
200 'FocalPlaneYResolution' => Exif::RATIONAL, # Focal plane Y resolution
201 'FocalPlaneResolutionUnit' => Exif::SHORT, # Focal plane resolution unit #p46
202 'SubjectLocation' => array( Exif::SHORT, 2), # Subject location
203 'ExposureIndex' => Exif::RATIONAL, # Exposure index
204 'SensingMethod' => Exif::SHORT, # Sensing method #p46
205 'FileSource' => Exif::UNDEFINED, # File source #p47
206 'SceneType' => Exif::UNDEFINED, # Scene type #p47
207 'CFAPattern' => Exif::UNDEFINED, # CFA pattern
208 'CustomRendered' => Exif::SHORT, # Custom image processing #p48
209 'ExposureMode' => Exif::SHORT, # Exposure mode #p48
210 'WhiteBalance' => Exif::SHORT, # White Balance #p49
211 'DigitalZoomRatio' => Exif::RATIONAL, # Digital zoom ration
212 'FocalLengthIn35mmFilm' => Exif::SHORT, # Focal length in 35 mm film
213 'SceneCaptureType' => Exif::SHORT, # Scene capture type #p49
214 'GainControl' => Exif::RATIONAL, # Scene control #p49-50
215 'Contrast' => Exif::SHORT, # Contrast #p50
216 'Saturation' => Exif::SHORT, # Saturation #p50
217 'Sharpness' => Exif::SHORT, # Sharpness #p50
218 'DeviceSettingDescription' => Exif::UNDEFINED, # Desice settings description
219 'SubjectDistanceRange' => Exif::SHORT, # Subject distance range #p51
220
221 'ImageUniqueID' => Exif::ASCII, # Unique image ID
222 ),
223
224 # GPS Attribute Information (p52)
225 'GPS' => array(
226 'GPSVersionID' => array( Exif::BYTE, 4 ), # GPS tag version
227 'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53
228 'GPSLatitude' => array( Exif::RATIONAL, 3 ), # Latitude
229 'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53
230 'GPSLongitude' => array( Exif::RATIONAL, 3), # Longitude
231 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference
232 'GPSAltitude' => Exif::RATIONAL, # Altitude
233 'GPSTimeStamp' => array( Exif::RATIONAL, 3), # GPS time (atomic clock)
234 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement
235 'GPSStatus' => Exif::ASCII, # Receiver status #p54
236 'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55
237 'GPSDOP' => Exif::RATIONAL, # Measurement precision
238 'GPSSpeedRef' => Exif::ASCII, # Speed unit #p55
239 'GPSSpeed' => Exif::RATIONAL, # Speed of GPS receiver
240 'GPSTrackRef' => Exif::ASCII, # Reference for direction of movement #p55
241 'GPSTrack' => Exif::RATIONAL, # Direction of movement
242 'GPSImgDirectionRef' => Exif::ASCII, # Reference for direction of image #p56
243 'GPSImgDirection' => Exif::RATIONAL, # Direction of image
244 'GPSMapDatum' => Exif::ASCII, # Geodetic survey data used
245 'GPSDestLatitudeRef' => Exif::ASCII, # Reference for latitude of destination #p56
246 'GPSDestLatitude' => array( Exif::RATIONAL, 3 ), # Latitude destination
247 'GPSDestLongitudeRef' => Exif::ASCII, # Reference for longitude of destination #p57
248 'GPSDestLongitude' => array( Exif::RATIONAL, 3 ), # Longitude of destination
249 'GPSDestBearingRef' => Exif::ASCII, # Reference for bearing of destination #p57
250 'GPSDestBearing' => Exif::RATIONAL, # Bearing of destination
251 'GPSDestDistanceRef' => Exif::ASCII, # Reference for distance to destination #p57-58
252 'GPSDestDistance' => Exif::RATIONAL, # Distance to destination
253 'GPSProcessingMethod' => Exif::UNDEFINED, # Name of GPS processing method
254 'GPSAreaInformation' => Exif::UNDEFINED, # Name of GPS area
255 'GPSDateStamp' => Exif::ASCII, # GPS date
256 'GPSDifferential' => Exif::SHORT, # GPS differential correction
257 ),
258 );
259
260 $this->file = $file;
261 $this->basename = wfBaseName( $this->file );
262
263 $this->debugFile( $this->basename, __FUNCTION__, true );
264 wfSuppressWarnings();
265 $data = exif_read_data( $this->file, 0, true );
266 wfRestoreWarnings();
267 /**
268 * exif_read_data() will return false on invalid input, such as
269 * when somebody uploads a file called something.jpeg
270 * containing random gibberish.
271 */
272 $this->mRawExifData = $data ? $data : array();
273 $this->makeFilteredData();
274 $this->makeFormattedData();
275 $this->debugFile( __FUNCTION__, false );
276 }
277
278 /**
279 * Make $this->mFilteredExifData
280 */
281 function makeFilteredData() {
282 $this->mFilteredExifData = $this->mRawExifData;
283
284 foreach( array_keys( $this->mFilteredExifData ) as $section ) {
285 if ( !in_array( $section, array_keys( $this->mExifTags ) ) ) {
286 $this->debug( $section , __FUNCTION__, "'$section' is not a valid Exif section" );
287 unset( $this->mFilteredExifData[$section] );
288 continue;
289 }
290
291 foreach( array_keys( $this->mFilteredExifData[$section] ) as $tag ) {
292 if ( !in_array( $tag, array_keys( $this->mExifTags[$section] ) ) ) {
293 $this->debug( $tag, __FUNCTION__, "'$tag' is not a valid tag in '$section'" );
294 unset( $this->mFilteredExifData[$section][$tag] );
295 continue;
296 }
297 $value = $this->mFilteredExifData[$section][$tag];
298 if( !$this->validate( $section, $tag, $value ) ) {
299 $this->debug( $value, __FUNCTION__, "'$tag' contained invalid data" );
300 unset( $this->mFilteredExifData[$section][$tag] );
301 }
302 }
303 }
304 }
305
306 /**
307 * @todo document
308 */
309 function makeFormattedData( ) {
310 $format = new FormatExif( $this->getFilteredData() );
311 $this->mFormattedExifData = $format->getFormattedData();
312 }
313 /**#@-*/
314
315 /**#@+
316 * @return array
317 */
318 /**
319 * Get $this->mRawExifData
320 */
321 function getData() {
322 return $this->mRawExifData;
323 }
324
325 /**
326 * Get $this->mFilteredExifData
327 */
328 function getFilteredData() {
329 return $this->mFilteredExifData;
330 }
331
332 /**
333 * Get $this->mFormattedExifData
334 */
335 function getFormattedData() {
336 return $this->mFormattedExifData;
337 }
338 /**#@-*/
339
340 /**
341 * The version of the output format
342 *
343 * Before the actual metadata information is saved in the database we
344 * strip some of it since we don't want to save things like thumbnails
345 * which usually accompany Exif data. This value gets saved in the
346 * database along with the actual Exif data, and if the version in the
347 * database doesn't equal the value returned by this function the Exif
348 * data is regenerated.
349 *
350 * @return int
351 */
352 public static function version() {
353 return 2; // We don't need no bloddy constants!
354 }
355
356 /**#@+
357 * Validates if a tag value is of the type it should be according to the Exif spec
358 *
359 * @private
360 *
361 * @param $in Mixed: the input value to check
362 * @return bool
363 */
364 function isByte( $in ) {
365 if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) {
366 $this->debug( $in, __FUNCTION__, true );
367 return true;
368 } else {
369 $this->debug( $in, __FUNCTION__, false );
370 return false;
371 }
372 }
373
374 function isASCII( $in ) {
375 if ( is_array( $in ) ) {
376 return false;
377 }
378
379 if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
380 $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' );
381 return false;
382 }
383
384 if ( preg_match( '/^\s*$/', $in ) ) {
385 $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
386 return false;
387 }
388
389 return true;
390 }
391
392 function isShort( $in ) {
393 if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) {
394 $this->debug( $in, __FUNCTION__, true );
395 return true;
396 } else {
397 $this->debug( $in, __FUNCTION__, false );
398 return false;
399 }
400 }
401
402 function isLong( $in ) {
403 if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) {
404 $this->debug( $in, __FUNCTION__, true );
405 return true;
406 } else {
407 $this->debug( $in, __FUNCTION__, false );
408 return false;
409 }
410 }
411
412 function isRational( $in ) {
413 $m = array();
414 if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
415 return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
416 } else {
417 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
418 return false;
419 }
420 }
421
422 function isUndefined( $in ) {
423 if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion
424 $this->debug( $in, __FUNCTION__, true );
425 return true;
426 } else {
427 $this->debug( $in, __FUNCTION__, false );
428 return false;
429 }
430 }
431
432 function isSlong( $in ) {
433 if ( $this->isLong( abs( $in ) ) ) {
434 $this->debug( $in, __FUNCTION__, true );
435 return true;
436 } else {
437 $this->debug( $in, __FUNCTION__, false );
438 return false;
439 }
440 }
441
442 function isSrational( $in ) {
443 $m = array();
444 if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
445 return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
446 } else {
447 $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
448 return false;
449 }
450 }
451 /**#@-*/
452
453 /**
454 * Validates if a tag has a legal value according to the Exif spec
455 *
456 * @private
457 * @param $section String: section where tag is located.
458 * @param $tag String: the tag to check.
459 * @param $val Mixed: the value of the tag.
460 * @param $recursive Boolean: true if called recursively for array types.
461 * @return bool
462 */
463 function validate( $section, $tag, $val, $recursive = false ) {
464 $debug = "tag is '$tag'";
465 $etype = $this->mExifTags[$section][$tag];
466 $ecount = 1;
467 if( is_array( $etype ) ) {
468 list( $etype, $ecount ) = $etype;
469 if ( $recursive )
470 $ecount = 1; // checking individual elements
471 }
472 $count = count( $val );
473 if( $ecount != $count ) {
474 $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
475 return false;
476 }
477 if( $count > 1 ) {
478 foreach( $val as $v ) {
479 if( !$this->validate( $section, $tag, $v, true ) ) {
480 return false;
481 }
482 }
483 return true;
484 }
485 // Does not work if not typecast
486 switch( (string)$etype ) {
487 case (string)Exif::BYTE:
488 $this->debug( $val, __FUNCTION__, $debug );
489 return $this->isByte( $val );
490 case (string)Exif::ASCII:
491 $this->debug( $val, __FUNCTION__, $debug );
492 return $this->isASCII( $val );
493 case (string)Exif::SHORT:
494 $this->debug( $val, __FUNCTION__, $debug );
495 return $this->isShort( $val );
496 case (string)Exif::LONG:
497 $this->debug( $val, __FUNCTION__, $debug );
498 return $this->isLong( $val );
499 case (string)Exif::RATIONAL:
500 $this->debug( $val, __FUNCTION__, $debug );
501 return $this->isRational( $val );
502 case (string)Exif::UNDEFINED:
503 $this->debug( $val, __FUNCTION__, $debug );
504 return $this->isUndefined( $val );
505 case (string)Exif::SLONG:
506 $this->debug( $val, __FUNCTION__, $debug );
507 return $this->isSlong( $val );
508 case (string)Exif::SRATIONAL:
509 $this->debug( $val, __FUNCTION__, $debug );
510 return $this->isSrational( $val );
511 case (string)Exif::SHORT.','.Exif::LONG:
512 $this->debug( $val, __FUNCTION__, $debug );
513 return $this->isShort( $val ) || $this->isLong( $val );
514 default:
515 $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
516 return false;
517 }
518 }
519
520 /**
521 * Convenience function for debugging output
522 *
523 * @private
524 *
525 * @param $in Mixed:
526 * @param $fname String:
527 * @param $action Mixed: , default NULL.
528 */
529 function debug( $in, $fname, $action = NULL ) {
530 if ( !$this->log ) {
531 return;
532 }
533 $type = gettype( $in );
534 $class = ucfirst( __CLASS__ );
535 if ( $type === 'array' )
536 $in = print_r( $in, true );
537
538 if ( $action === true )
539 wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n");
540 elseif ( $action === false )
541 wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n");
542 elseif ( $action === null )
543 wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n");
544 else
545 wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n");
546 }
547
548 /**
549 * Convenience function for debugging output
550 *
551 * @private
552 *
553 * @param $fname String: the name of the function calling this function
554 * @param $io Boolean: Specify whether we're beginning or ending
555 */
556 function debugFile( $fname, $io ) {
557 if ( !$this->log ) {
558 return;
559 }
560 $class = ucfirst( __CLASS__ );
561 if ( $io ) {
562 wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" );
563 } else {
564 wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" );
565 }
566 }
567
568 }
569
570 /**
571 * @todo document (e.g. one-sentence class-overview description)
572 * @ingroup Media
573 */
574 class FormatExif {
575 /**
576 * The Exif data to format
577 *
578 * @var array
579 * @private
580 */
581 var $mExif;
582
583 /**
584 * Constructor
585 *
586 * @param $exif Array: the Exif data to format ( as returned by
587 * Exif::getFilteredData() )
588 */
589 function FormatExif( $exif ) {
590 $this->mExif = $exif;
591 }
592
593 /**
594 * Numbers given by Exif user agents are often magical, that is they
595 * should be replaced by a detailed explanation depending on their
596 * value which most of the time are plain integers. This function
597 * formats Exif values into human readable form.
598 *
599 * @return array
600 */
601 function getFormattedData() {
602 global $wgLang;
603
604 $sections =& $this->mExif;
605
606 $resolutionunit = !isset( $sections['IFD0']['ResolutionUnit'] ) || $sections['IFD0']['ResolutionUnit'] == 2 ? 2 : 3;
607 unset( $sections['IFD0']['ResolutionUnit'] );
608
609 foreach( $sections as $section => &$tags ) {
610 foreach( $tags as $tag => $val ) {
611 switch( $tag ) {
612 case 'Compression':
613 switch( $val ) {
614 case 1: case 6:
615 $tags[$tag] = $this->msg( $tag, $val );
616 break;
617 default:
618 $tags[$tag] = $val;
619 break;
620 }
621 break;
622
623 case 'PhotometricInterpretation':
624 switch( $val ) {
625 case 2: case 6:
626 $tags[$tag] = $this->msg( $tag, $val );
627 break;
628 default:
629 $tags[$tag] = $val;
630 break;
631 }
632 break;
633
634 case 'Orientation':
635 switch( $val ) {
636 case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
637 $tags[$tag] = $this->msg( $tag, $val );
638 break;
639 default:
640 $tags[$tag] = $val;
641 break;
642 }
643 break;
644
645 case 'PlanarConfiguration':
646 switch( $val ) {
647 case 1: case 2:
648 $tags[$tag] = $this->msg( $tag, $val );
649 break;
650 default:
651 $tags[$tag] = $val;
652 break;
653 }
654 break;
655
656 // TODO: YCbCrSubSampling
657 // TODO: YCbCrPositioning
658
659 case 'XResolution':
660 case 'YResolution':
661 switch( $resolutionunit ) {
662 case 2:
663 $tags[$tag] = $this->msg( 'XYResolution', 'i', $this->formatNum( $val ) );
664 break;
665 case 3:
666 $this->msg( 'XYResolution', 'c', $this->formatNum( $val ) );
667 break;
668 default:
669 $tags[$tag] = $val;
670 break;
671 }
672 break;
673
674 // TODO: YCbCrCoefficients #p27 (see annex E)
675 case 'ExifVersion': case 'FlashPixVersion':
676 $tags[$tag] = "$val"/100;
677 break;
678
679 case 'ColorSpace':
680 switch( $val ) {
681 case 1: case 'FFFF.H':
682 $tags[$tag] = $this->msg( $tag, $val );
683 break;
684 default:
685 $tags[$tag] = $val;
686 break;
687 }
688 break;
689
690 case 'ComponentsConfiguration':
691 switch( $val ) {
692 case 0: case 1: case 2: case 3: case 4: case 5: case 6:
693 $tags[$tag] = $this->msg( $tag, $val );
694 break;
695 default:
696 $tags[$tag] = $val;
697 break;
698 }
699 break;
700
701 case 'DateTime':
702 case 'DateTimeOriginal':
703 case 'DateTimeDigitized':
704 if( $val == '0000:00:00 00:00:00' ) {
705 $tags[$tag] = wfMsg('exif-unknowndate');
706 } elseif( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/', $val ) ) {
707 $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) );
708 }
709 break;
710
711 case 'ExposureProgram':
712 switch( $val ) {
713 case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
714 $tags[$tag] = $this->msg( $tag, $val );
715 break;
716 default:
717 $tags[$tag] = $val;
718 break;
719 }
720 break;
721
722 case 'SubjectDistance':
723 $tags[$tag] = $this->msg( $tag, '', $this->formatNum( $val ) );
724 break;
725
726 case 'MeteringMode':
727 switch( $val ) {
728 case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255:
729 $tags[$tag] = $this->msg( $tag, $val );
730 break;
731 default:
732 $tags[$tag] = $val;
733 break;
734 }
735 break;
736
737 case 'LightSource':
738 switch( $val ) {
739 case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11:
740 case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20:
741 case 21: case 22: case 23: case 24: case 255:
742 $tags[$tag] = $this->msg( $tag, $val );
743 break;
744 default:
745 $tags[$tag] = $val;
746 break;
747 }
748 break;
749
750 case 'Flash':
751 $flashDecode = array(
752 'fired' => $val & bindec( '00000001' ),
753 'return' => ($val & bindec( '00000110' )) >> 1,
754 'mode' => ($val & bindec( '00011000' )) >> 3,
755 'function' => ($val & bindec( '00100000' )) >> 5,
756 'redeye' => ($val & bindec( '01000000' )) >> 6,
757 // 'reserved' => ($val & bindec( '10000000' )) >> 7,
758 );
759
760 # We do not need to handle unknown values since all are used.
761 foreach( $flashDecode as $subTag => $subValue ) {
762 # We do not need any message for zeroed values.
763 if( $subTag != 'fired' && $subValue == 0) {
764 continue;
765 }
766 $fullTag = $tag . '-' . $subTag ;
767 $flashMsgs[] = $this->msg( $fullTag, $subValue );
768 }
769 $tags[$tag] = $wgLang->commaList( $flashMsgs );
770 break;
771
772 case 'FocalPlaneResolutionUnit':
773 switch( $val ) {
774 case 2:
775 $tags[$tag] = $this->msg( $tag, $val );
776 break;
777 default:
778 $tags[$tag] = $val;
779 break;
780 }
781 break;
782
783 case 'SensingMethod':
784 switch( $val ) {
785 case 1: case 2: case 3: case 4: case 5: case 7: case 8:
786 $tags[$tag] = $this->msg( $tag, $val );
787 break;
788 default:
789 $tags[$tag] = $val;
790 break;
791 }
792 break;
793
794 case 'FileSource':
795 switch( $val ) {
796 case 3:
797 $tags[$tag] = $this->msg( $tag, $val );
798 break;
799 default:
800 $tags[$tag] = $val;
801 break;
802 }
803 break;
804
805 case 'SceneType':
806 switch( $val ) {
807 case 1:
808 $tags[$tag] = $this->msg( $tag, $val );
809 break;
810 default:
811 $tags[$tag] = $val;
812 break;
813 }
814 break;
815
816 case 'CustomRendered':
817 switch( $val ) {
818 case 0: case 1:
819 $tags[$tag] = $this->msg( $tag, $val );
820 break;
821 default:
822 $tags[$tag] = $val;
823 break;
824 }
825 break;
826
827 case 'ExposureMode':
828 switch( $val ) {
829 case 0: case 1: case 2:
830 $tags[$tag] = $this->msg( $tag, $val );
831 break;
832 default:
833 $tags[$tag] = $val;
834 break;
835 }
836 break;
837
838 case 'WhiteBalance':
839 switch( $val ) {
840 case 0: case 1:
841 $tags[$tag] = $this->msg( $tag, $val );
842 break;
843 default:
844 $tags[$tag] = $val;
845 break;
846 }
847 break;
848
849 case 'SceneCaptureType':
850 switch( $val ) {
851 case 0: case 1: case 2: case 3:
852 $tags[$tag] = $this->msg( $tag, $val );
853 break;
854 default:
855 $tags[$tag] = $val;
856 break;
857 }
858 break;
859
860 case 'GainControl':
861 switch( $val ) {
862 case 0: case 1: case 2: case 3: case 4:
863 $tags[$tag] = $this->msg( $tag, $val );
864 break;
865 default:
866 $tags[$tag] = $val;
867 break;
868 }
869 break;
870
871 case 'Contrast':
872 switch( $val ) {
873 case 0: case 1: case 2:
874 $tags[$tag] = $this->msg( $tag, $val );
875 break;
876 default:
877 $tags[$tag] = $val;
878 break;
879 }
880 break;
881
882 case 'Saturation':
883 switch( $val ) {
884 case 0: case 1: case 2:
885 $tags[$tag] = $this->msg( $tag, $val );
886 break;
887 default:
888 $tags[$tag] = $val;
889 break;
890 }
891 break;
892
893 case 'Sharpness':
894 switch( $val ) {
895 case 0: case 1: case 2:
896 $tags[$tag] = $this->msg( $tag, $val );
897 break;
898 default:
899 $tags[$tag] = $val;
900 break;
901 }
902 break;
903
904 case 'SubjectDistanceRange':
905 switch( $val ) {
906 case 0: case 1: case 2: case 3:
907 $tags[$tag] = $this->msg( $tag, $val );
908 break;
909 default:
910 $tags[$tag] = $val;
911 break;
912 }
913 break;
914
915 case 'GPSLatitudeRef':
916 case 'GPSDestLatitudeRef':
917 switch( $val ) {
918 case 'N': case 'S':
919 $tags[$tag] = $this->msg( 'GPSLatitude', $val );
920 break;
921 default:
922 $tags[$tag] = $val;
923 break;
924 }
925 break;
926
927 case 'GPSLongitudeRef':
928 case 'GPSDestLongitudeRef':
929 switch( $val ) {
930 case 'E': case 'W':
931 $tags[$tag] = $this->msg( 'GPSLongitude', $val );
932 break;
933 default:
934 $tags[$tag] = $val;
935 break;
936 }
937 break;
938
939 case 'GPSAltitudeRef':
940 switch( $val ) {
941 case 0: case 1:
942 $tags[$tag] = $this->msg( 'GPSAltitude', $val );
943 break;
944 default:
945 $tags[$tag] = $val;
946 break;
947 }
948 break;
949
950 case 'GPSLatitude':
951 case 'GPSDestLatitude':
952 case 'GPSLongitude':
953 case 'GPSDestLongitude':
954 $tags[$tag] = $this->formatCoords( $val );
955 break;
956
957 case 'GPSStatus':
958 switch( $val ) {
959 case 'A': case 'V':
960 $tags[$tag] = $this->msg( $tag, $val );
961 break;
962 default:
963 $tags[$tag] = $val;
964 break;
965 }
966 break;
967
968 case 'GPSMeasureMode':
969 switch( $val ) {
970 case 2: case 3:
971 $tags[$tag] = $this->msg( $tag, $val );
972 break;
973 default:
974 $tags[$tag] = $val;
975 break;
976 }
977 break;
978
979 case 'GPSSpeedRef':
980 switch( $val ) {
981 case 'K': case 'M': case 'N':
982 $tags[$tag] = $this->msg( 'GPSSpeed', $val );
983 break;
984 default:
985 $tags[$tag] = $val;
986 break;
987 }
988 break;
989
990 case 'GPSDestDistanceRef':
991 switch( $val ) {
992 case 'K': case 'M': case 'N':
993 $tags[$tag] = $this->msg( 'GPSDestDistance', $val );
994 break;
995 default:
996 $tags[$tag] = $val;
997 break;
998 }
999 break;
1000
1001 case 'GPSTrackRef':
1002 case 'GPSImgDirectionRef':
1003 case 'GPSDestBearingRef':
1004 switch( $val ) {
1005 case 'T': case 'M':
1006 $tags[$tag] = $this->msg( 'GPSDirection', $val );
1007 break;
1008 default:
1009 $tags[$tag] = $val;
1010 break;
1011 }
1012 break;
1013
1014 case 'GPSDateStamp':
1015 $tags[$tag] = $wgLang->date( substr( $val, 0, 4 ) . substr( $val, 5, 2 ) . substr( $val, 8, 2 ) . '000000' );
1016 break;
1017
1018 // This is not in the Exif standard, just a special
1019 // case for our purposes which enables wikis to wikify
1020 // the make, model and software name to link to their articles.
1021 case 'Make':
1022 case 'Model':
1023 case 'Software':
1024 $tags[$tag] = $this->msg( $tag, '', $val );
1025 break;
1026
1027 case 'ExposureTime':
1028 // Show the pretty fraction as well as decimal version
1029 $tags[$tag] = wfMsg( 'exif-exposuretime-format',
1030 $this->formatFraction( $val ), $this->formatNum( $val ) );
1031 break;
1032
1033 case 'FNumber':
1034 $tags[$tag] = wfMsg( 'exif-fnumber-format',
1035 $this->formatNum( $val ) );
1036 break;
1037
1038 case 'FocalLength':
1039 $tags[$tag] = wfMsg( 'exif-focallength-format',
1040 $this->formatNum( $val ) );
1041 break;
1042
1043 default:
1044 $tags[$tag] = $this->formatNum( $val );
1045 break;
1046 }
1047 }
1048 }
1049
1050 return $this->mExif;
1051 }
1052
1053 /**
1054 * Convenience function for getFormattedData()
1055 *
1056 * @private
1057 *
1058 * @param $tag String: the tag name to pass on
1059 * @param $val String: the value of the tag
1060 * @param $arg String: an argument to pass ($1)
1061 * @return string A wfMsg of "exif-$tag-$val" in lower case
1062 */
1063 function msg( $tag, $val, $arg = null ) {
1064 global $wgContLang;
1065
1066 if ($val === '')
1067 $val = 'value';
1068 return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg );
1069 }
1070
1071 /**
1072 * Format a number, convert numbers from fractions into floating point
1073 * numbers, joins arrays of numbers with commas.
1074 *
1075 * @private
1076 *
1077 * @param $num Mixed: the value to format
1078 * @return mixed A floating point number or whatever we were fed
1079 */
1080 function formatNum( $num ) {
1081 global $wgLang;
1082 $m = array();
1083 if( is_array($num) ) {
1084 $out = array();
1085 foreach( $num as $number ) {
1086 $out[] = $this->formatNum($number);
1087 }
1088 return $wgLang->commaList( $out );
1089 }
1090 if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) )
1091 return $m[2] != 0 ? $m[1] / $m[2] : $num;
1092 else
1093 return $num;
1094 }
1095
1096 /**
1097 * Format a rational number, reducing fractions
1098 *
1099 * @private
1100 *
1101 * @param $num Mixed: the value to format
1102 * @return mixed A floating point number or whatever we were fed
1103 */
1104 function formatFraction( $num ) {
1105 $m = array();
1106 if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) {
1107 $numerator = intval( $m[1] );
1108 $denominator = intval( $m[2] );
1109 $gcd = $this->gcd( $numerator, $denominator );
1110 if( $gcd != 0 ) {
1111 // 0 shouldn't happen! ;)
1112 return $numerator / $gcd . '/' . $denominator / $gcd;
1113 }
1114 }
1115 return $this->formatNum( $num );
1116 }
1117
1118 /**
1119 * Calculate the greatest common divisor of two integers.
1120 *
1121 * @param $a Integer: FIXME
1122 * @param $b Integer: FIXME
1123 * @return int
1124 * @private
1125 */
1126 function gcd( $a, $b ) {
1127 /*
1128 // http://en.wikipedia.org/wiki/Euclidean_algorithm
1129 // Recursive form would be:
1130 if( $b == 0 )
1131 return $a;
1132 else
1133 return gcd( $b, $a % $b );
1134 */
1135 while( $b != 0 ) {
1136 $remainder = $a % $b;
1137
1138 // tail recursion...
1139 $a = $b;
1140 $b = $remainder;
1141 }
1142 return $a;
1143 }
1144
1145 /**
1146 * Format a coordinate value, convert numbers from fractions
1147 * into floating point numbers, .
1148 *
1149 * @private
1150 *
1151 * @param $coords Array: degrees, minutes and seconds
1152 * @param $ref String: reference direction (N/S/E/W), optional
1153 * @return mixed A floating point number or whatever we were fed
1154 */
1155 function formatCoords( $coords, $ref = null ) {
1156 list($deg, $min, $sec) = $coords;
1157 $deg = $this->formatNum($deg);
1158 $min = $this->formatNum($min);
1159 $sec = $this->formatNum($sec);
1160 $out = $deg . "°";
1161 if ($min) $out .= " " . $min . "'";
1162 if ($sec) $out .= " " . $sec . '"';
1163 if ($ref) $out .= " " . $ref;
1164 return $out;
1165 }
1166
1167 }
1168
1169 /**
1170 * MW 1.6 compatibility
1171 */
1172 define( 'MW_EXIF_BYTE', Exif::BYTE );
1173 define( 'MW_EXIF_ASCII', Exif::ASCII );
1174 define( 'MW_EXIF_SHORT', Exif::SHORT );
1175 define( 'MW_EXIF_LONG', Exif::LONG );
1176 define( 'MW_EXIF_RATIONAL', Exif::RATIONAL );
1177 define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED );
1178 define( 'MW_EXIF_SLONG', Exif::SLONG );
1179 define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL );