Merge to trunk everything in img_metadata branch.
authorBrian Wolff <bawolff@users.mediawiki.org>
Sat, 16 Apr 2011 01:23:15 +0000 (01:23 +0000)
committerBrian Wolff <bawolff@users.mediawiki.org>
Sat, 16 Apr 2011 01:23:15 +0000 (01:23 +0000)
Hope I did this in an ok fashion. svn merge --re-integrate was giving me issues
so I just essentially over-wrote my working copy with the version at img_metadata.

38 files changed:
docs/hooks.txt
includes/AutoLoader.php
includes/DefaultSettings.php
includes/Exif.php [deleted file]
includes/ImagePage.php
includes/api/ApiQueryImageInfo.php
includes/filerepo/File.php
includes/filerepo/ForeignAPIFile.php
includes/filerepo/LocalFile.php
includes/media/Bitmap.php
includes/media/BitmapMetadataHandler.php [new file with mode: 0644]
includes/media/Exif.php [new file with mode: 0644]
includes/media/FormatMetadata.php [new file with mode: 0644]
includes/media/GIF.php
includes/media/GIFMetadataExtractor.php
includes/media/Generic.php
includes/media/IPTC.php [new file with mode: 0644]
includes/media/Jpeg.php [new file with mode: 0644]
includes/media/JpegMetadataExtractor.php [new file with mode: 0644]
includes/media/PNG.php
includes/media/PNGMetadataExtractor.php
includes/media/XMP.php [new file with mode: 0644]
includes/media/XMPInfo.php [new file with mode: 0644]
includes/media/XMPValidate.php [new file with mode: 0644]
languages/messages/MessagesEn.php
languages/messages/MessagesFr.php
languages/messages/MessagesFrp.php
languages/messages/MessagesIt.php
languages/messages/MessagesJa.php
languages/messages/MessagesNl.php
languages/messages/MessagesOc.php
languages/messages/MessagesPms.php
languages/messages/MessagesQqq.php
languages/messages/MessagesScn.php
maintenance/language/messageTypes.inc
maintenance/language/messages.inc
maintenance/rebuildImages.php
skins/common/shared.css

index 399b520..8601c49 100644 (file)
@@ -877,6 +877,14 @@ $title: Title object of page
 $url: string value as output (out parameter, can modify)
 $query: query options passed to Title::getLocalURL()
 
+'GetMetadataVersion': modify the image metadata version currently in use. This is
+       used when requesting image metadata from a ForiegnApiRepo. Media handlers
+       that need to have versioned metadata should add an element to the end of
+       the version array of the form 'handler_name=version'. Most media handlers
+       won't need to do this unless they broke backwards compatibility with a
+       previous version of the media handler metadata output. 
+&$version: Array of version strings
+
 'GetPreferences': modify user preferences
 $user: User whose preferences are being modified.
 &$preferences: Preferences description array, to be fed to an HTMLForm object
@@ -1998,5 +2006,14 @@ $obj: The XmlDumpWriter object.
 $row: The database row for the revision.
 $text: The revision text.
 
+'XMPGetInfo': Called when obtaining the list of XMP tags to extract. Can be used to add
+       additional tags to extract.
+&$items: Array containing information on which items to extract. See XMPInfo for details on the format.
+
+'XMPGetResults': Called just before returning the results array of parsing xmp data. Can be
+       used to post-process the results.
+&$data: Array of metadata sections (such as $data['xmp-general']) each section is an array of
+       metadata tags returned (each tag is either a value, or an array of values).
+
 More hooks might be available but undocumented, you can execute
 ./maintenance/findhooks.php to find hidden one.
index 7ede724..eb03d94 100644 (file)
@@ -75,7 +75,7 @@ $wgAutoloadLocalClasses = array(
        'EmailNotification' => 'includes/UserMailer.php',
        'EnhancedChangesList' => 'includes/ChangesList.php',
        'ErrorPageError' => 'includes/Exception.php',
-       'Exif' => 'includes/Exif.php',
+       'Exif' => 'includes/media/Exif.php',
        'ExplodeIterator' => 'includes/StringUtils.php',
        'ExternalEdit' => 'includes/ExternalEdit.php',
        'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
@@ -93,7 +93,7 @@ $wgAutoloadLocalClasses = array(
        'FileDependency' => 'includes/CacheDependency.php',
        'FileRevertForm' => 'includes/FileRevertForm.php',
        'ForkController' => 'includes/ForkController.php',
-       'FormatExif' => 'includes/Exif.php',
+       'FormatExif' => 'includes/media/FormatMetadata.php',
        'FormOptions' => 'includes/FormOptions.php',
        'GenderCache' => 'includes/GenderCache.php',
        'GlobalDependency' => 'includes/CacheDependency.php',
@@ -516,6 +516,7 @@ $wgAutoloadLocalClasses = array(
        'BitmapHandler_ClientOnly' => 'includes/media/Bitmap_ClientOnly.php',
        'BmpHandler' => 'includes/media/BMP.php',
        'DjVuHandler' => 'includes/media/DjVu.php',
+       'FormatMetadata' => 'includes/media/FormatMetadata.php',
        'GIFHandler' => 'includes/media/GIF.php',
        'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php',
        'ImageHandler' => 'includes/media/Generic.php',
@@ -526,9 +527,16 @@ $wgAutoloadLocalClasses = array(
        'PNGMetadataExtractor' => 'includes/media/PNGMetadataExtractor.php',
        'SvgHandler' => 'includes/media/SVG.php',
        'SVGMetadataExtractor' => 'includes/media/SVGMetadataExtractor.php',
+       'JpegHandler' => 'includes/media/Jpeg.php',
+       'JpegMetadataExtractor' => 'includes/media/JpegMetadataExtractor.php',
+       'BitmapMetadataHandler' => 'includes/media/BitmapMetadataHandler.php',
+       'IPTC' => 'includes/media/IPTC.php',
        'ThumbnailImage' => 'includes/media/MediaTransformOutput.php',
        'TiffHandler' => 'includes/media/Tiff.php',
        'TransformParameterError' => 'includes/media/MediaTransformOutput.php',
+       'XMPReader' => 'includes/media/XMP.php',
+       'XMPInfo' => 'includes/media/XMPInfo.php',
+       'XMPValidate' => 'includes/media/XMPValidate.php',
 
        # includes/normal
        'UtfNormal' => 'includes/normal/UtfNormal.php',
index 3080df4..52609e7 100644 (file)
@@ -394,6 +394,13 @@ $wgUseInstantCommons = false;
  */
 $wgShowEXIF = function_exists( 'exif_read_data' );
 
+/**
+ * If to automatically update the img_metadata field
+ * if the metadata field is outdated but compatible with the current version.
+ * Defaults to false.
+ */
+$wgUpdateCompatibleMetadata = false;
+
 /**
  * If you operate multiple wikis, you can define a shared upload path here.
  * Uploads to this wiki will NOT be put there - they will be put into
@@ -592,7 +599,7 @@ $wgTrustedMediaFormats = array(
  * Each entry in the array maps a MIME type to a class name
  */
 $wgMediaHandlers = array(
-       'image/jpeg' => 'BitmapHandler',
+       'image/jpeg' => 'JpegHandler',
        'image/png' => 'PNGHandler',
        'image/gif' => 'GIFHandler',
        'image/tiff' => 'TiffHandler',
diff --git a/includes/Exif.php b/includes/Exif.php
deleted file mode 100644 (file)
index a009199..0000000
+++ /dev/null
@@ -1,1154 +0,0 @@
-<?php
-/**
- * Exif metadata reader
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @ingroup Media
- * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
- * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
- * @file
- */
-
-/**
- * @todo document (e.g. one-sentence class-overview description)
- * @ingroup Media
- */
-class Exif {
-
-       const BYTE      = 1;    //!< An 8-bit (1-byte) unsigned integer.
-       const ASCII     = 2;    //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
-       const SHORT     = 3;    //!< A 16-bit (2-byte) unsigned integer.
-       const LONG      = 4;    //!< A 32-bit (4-byte) unsigned integer.
-       const RATIONAL  = 5;    //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
-       const UNDEFINED = 7;    //!< An 8-bit byte that can take any value depending on the field definition
-       const SLONG     = 9;    //!< A 32-bit (4-byte) signed integer (2's complement notation),
-       const SRATIONAL = 10;   //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
-
-       //@{
-       /* @var array
-        * @private
-        */
-
-       /**
-        * Exif tags grouped by category, the tagname itself is the key and the type
-        * is the value, in the case of more than one possible value type they are
-        * separated by commas.
-        */
-       var $mExifTags;
-
-       /**
-        * A one dimentional array of all Exif tags
-        */
-       var $mFlatExifTags;
-
-       /**
-        * The raw Exif data returned by exif_read_data()
-        */
-       var $mRawExifData;
-
-       /**
-        * A Filtered version of $mRawExifData that has been pruned of invalid
-        * tags and tags that contain content they shouldn't contain according
-        * to the Exif specification
-        */
-       var $mFilteredExifData;
-
-       /**
-        * Filtered and formatted Exif data, see FormatExif::getFormattedData()
-        */
-       var $mFormattedExifData;
-
-       //@}
-
-       //@{
-       /* @var string
-        * @private
-        */
-
-       /**
-        * The file being processed
-        */
-       var $file;
-
-       /**
-        * The basename of the file being processed
-        */
-       var $basename;
-
-       /**
-        * The private log to log to, e.g. 'exif'
-        */
-       var $log = false;
-
-       //@}
-
-       /**
-        * Constructor
-        *
-        * @param $file String: filename.
-        */
-       function __construct( $file ) {
-               /**
-                * Page numbers here refer to pages in the EXIF 2.2 standard
-                *
-                * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification
-                */
-               $this->mExifTags = array(
-                       # TIFF Rev. 6.0 Attribute Information (p22)
-                       'tiff' => array(
-                               # Tags relating to image structure
-                               'structure' => array(
-                                       'ImageWidth' => Exif::SHORT.','.Exif::LONG,             # Image width
-                                       'ImageLength' => Exif::SHORT.','.Exif::LONG,    # Image height
-                                       'BitsPerSample' => Exif::SHORT,                 # Number of bits per component
-                                       # "When a primary image is JPEG compressed, this designation is not"
-                                       # "necessary and is omitted." (p23)
-                                       'Compression' => Exif::SHORT,                           # Compression scheme #p23
-                                       'PhotometricInterpretation' => Exif::SHORT,             # Pixel composition #p23
-                                       'Orientation' => Exif::SHORT,                           # Orientation of image #p24
-                                       'SamplesPerPixel' => Exif::SHORT,                       # Number of components
-                                       'PlanarConfiguration' => Exif::SHORT,                   # Image data arrangement #p24
-                                       'YCbCrSubSampling' => Exif::SHORT,                      # Subsampling ratio of Y to C #p24
-                                       'YCbCrPositioning' => Exif::SHORT,                      # Y and C positioning #p24-25
-                                       'XResolution' => Exif::RATIONAL,                        # Image resolution in width direction
-                                       'YResolution' => Exif::RATIONAL,                        # Image resolution in height direction
-                                       'ResolutionUnit' => Exif::SHORT,                        # Unit of X and Y resolution #(p26)
-                               ),
-
-                               # Tags relating to recording offset
-                               'offset' => array(
-                                       'StripOffsets' => Exif::SHORT.','.Exif::LONG,                   # Image data location
-                                       'RowsPerStrip' => Exif::SHORT.','.Exif::LONG,                   # Number of rows per strip
-                                       'StripByteCounts' => Exif::SHORT.','.Exif::LONG,                        # Bytes per compressed strip
-                                       'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG,          # Offset to JPEG SOI
-                                       'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG,    # Bytes of JPEG data
-                               ),
-
-                               # Tags relating to image data characteristics
-                               'characteristics' => array(
-                                       'TransferFunction' => Exif::SHORT,              # Transfer function
-                                       'WhitePoint' => Exif::RATIONAL,         # White point chromaticity
-                                       'PrimaryChromaticities' => Exif::RATIONAL,      # Chromaticities of primarities
-                                       'YCbCrCoefficients' => Exif::RATIONAL,  # Color space transformation matrix coefficients #p27
-                                       'ReferenceBlackWhite' => Exif::RATIONAL # Pair of black and white reference values
-                               ),
-
-                               # Other tags
-                               'other' => array(
-                                       'DateTime' => Exif::ASCII,            # File change date and time
-                                       'ImageDescription' => Exif::ASCII,    # Image title
-                                       'Make' => Exif::ASCII,                # Image input equipment manufacturer
-                                       'Model' => Exif::ASCII,               # Image input equipment model
-                                       'Software' => Exif::ASCII,            # Software used
-                                       'Artist' => Exif::ASCII,              # Person who created the image
-                                       'Copyright' => Exif::ASCII,           # Copyright holder
-                               ),
-                       ),
-
-                       # Exif IFD Attribute Information (p30-31)
-                       'exif' => array(
-                               # Tags relating to version
-                               'version' => array(
-                                       # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance
-                                       # to the EXIF 2.1 AND 2.2 standards
-                                       'ExifVersion' => Exif::UNDEFINED,       # Exif version
-                                       'FlashpixVersion' => Exif::UNDEFINED, # Supported Flashpix version #p32
-                               ),
-
-                               # Tags relating to Image Data Characteristics
-                               'characteristics' => array(
-                                       'ColorSpace' => Exif::SHORT,            # Color space information #p32
-                               ),
-
-                               # Tags relating to image configuration
-                               'configuration' => array(
-                                       'ComponentsConfiguration' => Exif::UNDEFINED,           # Meaning of each component #p33
-                                       'CompressedBitsPerPixel' => Exif::RATIONAL,             # Image compression mode
-                                       'PixelYDimension' => Exif::SHORT.','.Exif::LONG,        # Valid image width
-                                       'PixelXDimension' => Exif::SHORT.','.Exif::LONG,        # Valind image height
-                               ),
-
-                               # Tags relating to related user information
-                               'user' => array(
-                                       'MakerNote' => Exif::UNDEFINED,                 # Manufacturer notes
-                                       'UserComment' => Exif::UNDEFINED,                       # User comments #p34
-                               ),
-
-                               # Tags relating to related file information
-                               'related' => array(
-                                       'RelatedSoundFile' => Exif::ASCII,                      # Related audio file
-                               ),
-
-                               # Tags relating to date and time
-                               'dateandtime' => array(
-                                       'DateTimeOriginal' => Exif::ASCII,                      # Date and time of original data generation #p36
-                                       'DateTimeDigitized' => Exif::ASCII,                     # Date and time of original data generation
-                                       'SubSecTime' => Exif::ASCII,                            # DateTime subseconds
-                                       'SubSecTimeOriginal' => Exif::ASCII,                    # DateTimeOriginal subseconds
-                                       'SubSecTimeDigitized' => Exif::ASCII,                   # DateTimeDigitized subseconds
-                               ),
-
-                               # Tags relating to picture-taking conditions (p31)
-                               'conditions' => array(
-                                       'ExposureTime' => Exif::RATIONAL,                       # Exposure time
-                                       'FNumber' => Exif::RATIONAL,                            # F Number
-                                       'ExposureProgram' => Exif::SHORT,                       # Exposure Program #p38
-                                       'SpectralSensitivity' => Exif::ASCII,                   # Spectral sensitivity
-                                       'ISOSpeedRatings' => Exif::SHORT,                       # ISO speed rating
-                                       'OECF' => Exif::UNDEFINED,                              # Optoelectronic conversion factor
-                                       'ShutterSpeedValue' => Exif::SRATIONAL,         # Shutter speed
-                                       'ApertureValue' => Exif::RATIONAL,                      # Aperture
-                                       'BrightnessValue' => Exif::SRATIONAL,                   # Brightness
-                                       'ExposureBiasValue' => Exif::SRATIONAL,         # Exposure bias
-                                       'MaxApertureValue' => Exif::RATIONAL,                   # Maximum land aperture
-                                       'SubjectDistance' => Exif::RATIONAL,                    # Subject distance
-                                       'MeteringMode' => Exif::SHORT,                  # Metering mode #p40
-                                       'LightSource' => Exif::SHORT,                           # Light source #p40-41
-                                       'Flash' => Exif::SHORT,                         # Flash #p41-42
-                                       'FocalLength' => Exif::RATIONAL,                        # Lens focal length
-                                       'SubjectArea' => Exif::SHORT,                           # Subject area
-                                       'FlashEnergy' => Exif::RATIONAL,                        # Flash energy
-                                       'SpatialFrequencyResponse' => Exif::UNDEFINED,  # Spatial frequency response
-                                       'FocalPlaneXResolution' => Exif::RATIONAL,              # Focal plane X resolution
-                                       'FocalPlaneYResolution' => Exif::RATIONAL,              # Focal plane Y resolution
-                                       'FocalPlaneResolutionUnit' => Exif::SHORT,              # Focal plane resolution unit #p46
-                                       'SubjectLocation' => Exif::SHORT,                       # Subject location
-                                       'ExposureIndex' => Exif::RATIONAL,                      # Exposure index
-                                       'SensingMethod' => Exif::SHORT,                 # Sensing method #p46
-                                       'FileSource' => Exif::UNDEFINED,                        # File source #p47
-                                       'SceneType' => Exif::UNDEFINED,                 # Scene type #p47
-                                       'CFAPattern' => Exif::UNDEFINED,                        # CFA pattern
-                                       'CustomRendered' => Exif::SHORT,                        # Custom image processing #p48
-                                       'ExposureMode' => Exif::SHORT,                  # Exposure mode #p48
-                                       'WhiteBalance' => Exif::SHORT,                  # White Balance #p49
-                                       'DigitalZoomRatio' => Exif::RATIONAL,                   # Digital zoom ration
-                                       'FocalLengthIn35mmFilm' => Exif::SHORT,         # Focal length in 35 mm film
-                                       'SceneCaptureType' => Exif::SHORT,                      # Scene capture type #p49
-                                       'GainControl' => Exif::RATIONAL,                        # Scene control #p49-50
-                                       'Contrast' => Exif::SHORT,                              # Contrast #p50
-                                       'Saturation' => Exif::SHORT,                            # Saturation #p50
-                                       'Sharpness' => Exif::SHORT,                             # Sharpness #p50
-                                       'DeviceSettingDescription' => Exif::UNDEFINED,  # Desice settings description
-                                       'SubjectDistanceRange' => Exif::SHORT,          # Subject distance range #p51
-                               ),
-
-                               'other' => array(
-                                       'ImageUniqueID' => Exif::ASCII, # Unique image ID
-                               ),
-                       ),
-
-                       # GPS Attribute Information (p52)
-                       'gps' => array(
-                               'GPSVersionID' => Exif::BYTE,                   # GPS tag version
-                               'GPSLatitudeRef' => Exif::ASCII,                # North or South Latitude #p52-53
-                               'GPSLatitude' => Exif::RATIONAL,                # Latitude
-                               'GPSLongitudeRef' => Exif::ASCII,               # East or West Longitude #p53
-                               'GPSLongitude' => Exif::RATIONAL,               # Longitude
-                               'GPSAltitudeRef' => Exif::BYTE,         # Altitude reference
-                               'GPSAltitude' => Exif::RATIONAL,                # Altitude
-                               'GPSTimeStamp' => Exif::RATIONAL,               # GPS time (atomic clock)
-                               'GPSSatellites' => Exif::ASCII,         # Satellites used for measurement
-                               'GPSStatus' => Exif::ASCII,                     # Receiver status #p54
-                               'GPSMeasureMode' => Exif::ASCII,                # Measurement mode #p54-55
-                               'GPSDOP' => Exif::RATIONAL,                     # Measurement precision
-                               'GPSSpeedRef' => Exif::ASCII,                   # Speed unit #p55
-                               'GPSSpeed' => Exif::RATIONAL,                   # Speed of GPS receiver
-                               'GPSTrackRef' => Exif::ASCII,                   # Reference for direction of movement #p55
-                               'GPSTrack' => Exif::RATIONAL,                   # Direction of movement
-                               'GPSImgDirectionRef' => Exif::ASCII,            # Reference for direction of image #p56
-                               'GPSImgDirection' => Exif::RATIONAL,            # Direction of image
-                               'GPSMapDatum' => Exif::ASCII,                   # Geodetic survey data used
-                               'GPSDestLatitudeRef' => Exif::ASCII,            # Reference for latitude of destination #p56
-                               'GPSDestLatitude' => Exif::RATIONAL,            # Latitude destination
-                               'GPSDestLongitudeRef' => Exif::ASCII,           # Reference for longitude of destination #p57
-                               'GPSDestLongitude' => Exif::RATIONAL,           # Longitude of destination
-                               'GPSDestBearingRef' => Exif::ASCII,             # Reference for bearing of destination #p57
-                               'GPSDestBearing' => Exif::RATIONAL,             # Bearing of destination
-                               'GPSDestDistanceRef' => Exif::ASCII,            # Reference for distance to destination #p57-58
-                               'GPSDestDistance' => Exif::RATIONAL,            # Distance to destination
-                               'GPSProcessingMethod' => Exif::UNDEFINED,       # Name of GPS processing method
-                               'GPSAreaInformation' => Exif::UNDEFINED,        # Name of GPS area
-                               'GPSDateStamp' => Exif::ASCII,          # GPS date
-                               'GPSDifferential' => Exif::SHORT,               # GPS differential correction
-                       ),
-               );
-
-               $this->file = $file;
-               $this->basename = wfBaseName( $this->file );
-
-               $this->makeFlatExifTags();
-
-               $this->debugFile( $this->basename, __FUNCTION__, true );
-               if( function_exists( 'exif_read_data' ) ) {
-                       wfSuppressWarnings();
-                       $data = exif_read_data( $this->file );
-                       wfRestoreWarnings();
-               } else {
-                       throw new MWException( "Internal error: exif_read_data not present. \$wgShowEXIF may be incorrectly set or not checked by an extension." );
-               }
-               /**
-                * exif_read_data() will return false on invalid input, such as
-                * when somebody uploads a file called something.jpeg
-                * containing random gibberish.
-                */
-               $this->mRawExifData = $data ? $data : array();
-
-               $this->makeFilteredData();
-               $this->makeFormattedData();
-
-               $this->debugFile( __FUNCTION__, false );
-       }
-
-       /**#@+
-        * @private
-        */
-       /**
-        * Generate a flat list of the exif tags
-        */
-       function makeFlatExifTags() {
-               $this->extractTags( $this->mExifTags );
-       }
-
-       /**
-        * A recursing extractor function used by makeFlatExifTags()
-        *
-        * Note: This used to use an array_walk function, but it made PHP5
-        * segfault, see `cvs diff -u -r 1.4 -r 1.5 Exif.php`
-        */
-       function extractTags( &$tagset ) {
-               foreach( $tagset as $key => $val ) {
-                       if( is_array( $val ) ) {
-                               $this->extractTags( $val );
-                       } else {
-                               $this->mFlatExifTags[$key] = $val;
-                       }
-               }
-       }
-
-       /**
-        * Make $this->mFilteredExifData
-        */
-       function makeFilteredData() {
-               $this->mFilteredExifData = $this->mRawExifData;
-
-               foreach( $this->mFilteredExifData as $k => $v ) {
-                       if ( !in_array( $k, array_keys( $this->mFlatExifTags ) ) ) {
-                               $this->debug( $v, __FUNCTION__, "'$k' is not a valid Exif tag" );
-                               unset( $this->mFilteredExifData[$k] );
-                       }
-               }
-
-               foreach( $this->mFilteredExifData as $k => $v ) {
-                       if ( !$this->validate($k, $v) ) {
-                               $this->debug( $v, __FUNCTION__, "'$k' contained invalid data" );
-                               unset( $this->mFilteredExifData[$k] );
-                       }
-               }
-       }
-
-       /**
-        * @todo document
-        */
-       function makeFormattedData( ) {
-               $format = new FormatExif( $this->getFilteredData() );
-               $this->mFormattedExifData = $format->getFormattedData();
-       }
-       /**#@-*/
-
-       /**#@+
-        * @return array
-        */
-       /**
-        * Get $this->mRawExifData
-        */
-       function getData() {
-               return $this->mRawExifData;
-       }
-
-       /**
-        * Get $this->mFilteredExifData
-        */
-       function getFilteredData() {
-               return $this->mFilteredExifData;
-       }
-
-       /**
-        * Get $this->mFormattedExifData
-        */
-       function getFormattedData() {
-               return $this->mFormattedExifData;
-       }
-       /**#@-*/
-
-       /**
-        * The version of the output format
-        *
-        * Before the actual metadata information is saved in the database we
-        * strip some of it since we don't want to save things like thumbnails
-        * which usually accompany Exif data. This value gets saved in the
-        * database along with the actual Exif data, and if the version in the
-        * database doesn't equal the value returned by this function the Exif
-        * data is regenerated.
-        *
-        * @return int
-        */
-       public static function version() {
-               return 1; // We don't need no bloddy constants!
-       }
-
-       /**#@+
-        * Validates if a tag value is of the type it should be according to the Exif spec
-        *
-        * @private
-        *
-        * @param $in Mixed: the input value to check
-        * @return bool
-        */
-       function isByte( $in ) {
-               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) {
-                       $this->debug( $in, __FUNCTION__, true );
-                       return true;
-               } else {
-                       $this->debug( $in, __FUNCTION__, false );
-                       return false;
-               }
-       }
-
-       function isASCII( $in ) {
-               if ( is_array( $in ) ) {
-                       return false;
-               }
-
-               if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
-                       $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' );
-                       return false;
-               }
-
-               if ( preg_match( '/^\s*$/', $in ) ) {
-                       $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
-                       return false;
-               }
-
-               return true;
-       }
-
-       function isShort( $in ) {
-               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) {
-                       $this->debug( $in, __FUNCTION__, true );
-                       return true;
-               } else {
-                       $this->debug( $in, __FUNCTION__, false );
-                       return false;
-               }
-       }
-
-       function isLong( $in ) {
-               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) {
-                       $this->debug( $in, __FUNCTION__, true );
-                       return true;
-               } else {
-                       $this->debug( $in, __FUNCTION__, false );
-                       return false;
-               }
-       }
-
-       function isRational( $in ) {
-               $m = array();
-               if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
-                       return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
-               } else {
-                       $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
-                       return false;
-               }
-       }
-
-       function isUndefined( $in ) {
-               if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion
-                       $this->debug( $in, __FUNCTION__, true );
-                       return true;
-               } else {
-                       $this->debug( $in, __FUNCTION__, false );
-                       return false;
-               }
-       }
-
-       function isSlong( $in ) {
-               if ( $this->isLong( abs( $in ) ) ) {
-                       $this->debug( $in, __FUNCTION__, true );
-                       return true;
-               } else {
-                       $this->debug( $in, __FUNCTION__, false );
-                       return false;
-               }
-       }
-
-       function isSrational( $in ) {
-               $m = array();
-               if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
-                       return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
-               } else {
-                       $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
-                       return false;
-               }
-       }
-       /**#@-*/
-
-       /**
-        * Validates if a tag has a legal value according to the Exif spec
-        *
-        * @private
-        *
-        * @param $tag String: the tag to check.
-        * @param $val Mixed: the value of the tag.
-        * @return bool
-        */
-       function validate( $tag, $val ) {
-               $debug = "tag is '$tag'";
-               // Does not work if not typecast
-               switch( (string)$this->mFlatExifTags[$tag] ) {
-                       case (string)Exif::BYTE:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isByte( $val );
-                       case (string)Exif::ASCII:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isASCII( $val );
-                       case (string)Exif::SHORT:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isShort( $val );
-                       case (string)Exif::LONG:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isLong( $val );
-                       case (string)Exif::RATIONAL:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isRational( $val );
-                       case (string)Exif::UNDEFINED:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isUndefined( $val );
-                       case (string)Exif::SLONG:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isSlong( $val );
-                       case (string)Exif::SRATIONAL:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isSrational( $val );
-                       case (string)Exif::SHORT.','.Exif::LONG:
-                               $this->debug( $val, __FUNCTION__, $debug );
-                               return $this->isShort( $val ) || $this->isLong( $val );
-                       default:
-                               $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
-                               return false;
-               }
-       }
-
-       /**
-        * Convenience function for debugging output
-        *
-        * @private
-        *
-        * @param $in Mixed:
-        * @param $fname String:
-        * @param $action Mixed: , default NULL.
-        */
-       function debug( $in, $fname, $action = null ) {
-               if ( !$this->log ) {
-                       return;
-               }
-               $type = gettype( $in );
-               $class = ucfirst( __CLASS__ );
-               if ( $type === 'array' )
-                       $in = print_r( $in, true );
-
-               if ( $action === true )
-                       wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n");
-               elseif ( $action === false )
-                       wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n");
-               elseif ( $action === null )
-                       wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n");
-               else
-                       wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n");
-       }
-
-       /**
-        * Convenience function for debugging output
-        *
-        * @private
-        *
-        * @param $fname String: the name of the function calling this function
-        * @param $io Boolean: Specify whether we're beginning or ending
-        */
-       function debugFile( $fname, $io ) {
-               if ( !$this->log ) {
-                       return;
-               }
-               $class = ucfirst( __CLASS__ );
-               if ( $io ) {
-                       wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" );
-               } else {
-                       wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" );
-               }
-       }
-
-}
-
-/**
- * @todo document (e.g. one-sentence class-overview description)
- * @ingroup Media
- */
-class FormatExif {
-       /**
-        * The Exif data to format
-        *
-        * @var array
-        * @private
-        */
-       var $mExif;
-
-       /**
-        * Constructor
-        *
-        * @param $exif Array: the Exif data to format ( as returned by
-        *                    Exif::getFilteredData() )
-        */
-       function __construct( $exif ) {
-               $this->mExif = $exif;
-       }
-
-       /**
-        * Numbers given by Exif user agents are often magical, that is they
-        * should be replaced by a detailed explanation depending on their
-        * value which most of the time are plain integers. This function
-        * formats Exif values into human readable form.
-        *
-        * @return array
-        */
-       function getFormattedData() {
-               global $wgLang;
-
-               $tags =& $this->mExif;
-
-               $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
-               unset( $tags['ResolutionUnit'] );
-
-               foreach( $tags as $tag => $val ) {
-                       switch( $tag ) {
-                       case 'Compression':
-                               switch( $val ) {
-                               case 1: case 6:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'PhotometricInterpretation':
-                               switch( $val ) {
-                               case 2: case 6:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'Orientation':
-                               switch( $val ) {
-                               case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'PlanarConfiguration':
-                               switch( $val ) {
-                               case 1: case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       // TODO: YCbCrSubSampling
-                       // TODO: YCbCrPositioning
-
-                       case 'XResolution':
-                       case 'YResolution':
-                               switch( $resolutionunit ) {
-                                       case 2:
-                                               $tags[$tag] = $this->msg( 'XYResolution', 'i', $this->formatNum( $val ) );
-                                               break;
-                                       case 3:
-                                               $this->msg( 'XYResolution', 'c', $this->formatNum( $val ) );
-                                               break;
-                                       default:
-                                               $tags[$tag] = $val;
-                                               break;
-                               }
-                               break;
-
-                       // TODO: YCbCrCoefficients  #p27 (see annex E)
-                       case 'ExifVersion': case 'FlashpixVersion':
-                               $tags[$tag] = "$val"/100;
-                               break;
-
-                       case 'ColorSpace':
-                               switch( $val ) {
-                               case 1: case 'FFFF.H':
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'ComponentsConfiguration':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3: case 4: case 5: case 6:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'DateTime':
-                       case 'DateTimeOriginal':
-                       case 'DateTimeDigitized':
-                               if( $val == '0000:00:00 00:00:00' ) {
-                                       $tags[$tag] = wfMsg('exif-unknowndate');
-                               } elseif( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/', $val ) ) {
-                                       $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) );
-                               }
-                               break;
-
-                       case 'ExposureProgram':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'SubjectDistance':
-                               $tags[$tag] = $this->msg( $tag, '', $this->formatNum( $val ) );
-                               break;
-
-                       case 'MeteringMode':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'LightSource':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11:
-                               case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20:
-                               case 21: case 22: case 23: case 24: case 255:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'Flash':
-                               $flashDecode = array(
-                                       'fired'    => $val & bindec( '00000001' ),
-                                       'return'   => ($val & bindec( '00000110' )) >> 1,
-                                       'mode'     => ($val & bindec( '00011000' )) >> 3,
-                                       'function' => ($val & bindec( '00100000' )) >> 5,
-                                       'redeye'   => ($val & bindec( '01000000' )) >> 6,
-//                                     'reserved' => ($val & bindec( '10000000' )) >> 7,
-                               );
-
-                               # We do not need to handle unknown values since all are used.
-                               foreach( $flashDecode as $subTag => $subValue ) {
-                                       # We do not need any message for zeroed values.
-                                       if( $subTag != 'fired' && $subValue == 0) {
-                                               continue;
-                                       }
-                                       $fullTag = $tag . '-' . $subTag ;
-                                       $flashMsgs[] = $this->msg( $fullTag, $subValue );
-                               }
-                               $tags[$tag] = $wgLang->commaList( $flashMsgs );
-                       break;
-
-                       case 'FocalPlaneResolutionUnit':
-                               switch( $val ) {
-                               case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'SensingMethod':
-                               switch( $val ) {
-                               case 1: case 2: case 3: case 4: case 5: case 7: case 8:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'FileSource':
-                               switch( $val ) {
-                               case 3:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'SceneType':
-                               switch( $val ) {
-                               case 1:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'CustomRendered':
-                               switch( $val ) {
-                               case 0: case 1:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'ExposureMode':
-                               switch( $val ) {
-                               case 0: case 1: case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'WhiteBalance':
-                               switch( $val ) {
-                               case 0: case 1:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'SceneCaptureType':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GainControl':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3: case 4:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'Contrast':
-                               switch( $val ) {
-                               case 0: case 1: case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'Saturation':
-                               switch( $val ) {
-                               case 0: case 1: case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'Sharpness':
-                               switch( $val ) {
-                               case 0: case 1: case 2:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'SubjectDistanceRange':
-                               switch( $val ) {
-                               case 0: case 1: case 2: case 3:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSLatitudeRef':
-                       case 'GPSDestLatitudeRef':
-                               switch( $val ) {
-                               case 'N': case 'S':
-                                       $tags[$tag] = $this->msg( 'GPSLatitude', $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSLongitudeRef':
-                       case 'GPSDestLongitudeRef':
-                               switch( $val ) {
-                               case 'E': case 'W':
-                                       $tags[$tag] = $this->msg( 'GPSLongitude', $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSStatus':
-                               switch( $val ) {
-                               case 'A': case 'V':
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSMeasureMode':
-                               switch( $val ) {
-                               case 2: case 3:
-                                       $tags[$tag] = $this->msg( $tag, $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSSpeedRef':
-                       case 'GPSDestDistanceRef':
-                               switch( $val ) {
-                               case 'K': case 'M': case 'N':
-                                       $tags[$tag] = $this->msg( 'GPSSpeed', $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSTrackRef':
-                       case 'GPSImgDirectionRef':
-                       case 'GPSDestBearingRef':
-                               switch( $val ) {
-                               case 'T': case 'M':
-                                       $tags[$tag] = $this->msg( 'GPSDirection', $val );
-                                       break;
-                               default:
-                                       $tags[$tag] = $val;
-                                       break;
-                               }
-                               break;
-
-                       case 'GPSDateStamp':
-                               $tags[$tag] = $wgLang->date( substr( $val, 0, 4 ) . substr( $val, 5, 2 ) . substr( $val, 8, 2 ) . '000000' );
-                               break;
-
-                       // This is not in the Exif standard, just a special
-                       // case for our purposes which enables wikis to wikify
-                       // the make, model and software name to link to their articles.
-                       case 'Make':
-                       case 'Model':
-                       case 'Software':
-                               $tags[$tag] = $this->msg( $tag, '', $val );
-                               break;
-
-                       case 'ExposureTime':
-                               // Show the pretty fraction as well as decimal version
-                               $tags[$tag] = wfMsg( 'exif-exposuretime-format',
-                                       $this->formatFraction( $val ), $this->formatNum( $val ) );
-                               break;
-
-                       case 'FNumber':
-                               $tags[$tag] = wfMsg( 'exif-fnumber-format',
-                                       $this->formatNum( $val ) );
-                               break;
-
-                       case 'FocalLength':
-                               $tags[$tag] = wfMsg( 'exif-focallength-format',
-                                       $this->formatNum( $val ) );
-                               break;
-
-                       // Do not transform fields with pure text.
-                       // For some languages the formatNum() conversion results to wrong output like
-                       // foo,bar@example,com or foo٫bar@example٫com
-                       case 'ImageDescription':
-                       case 'Artist':
-                       case 'Copyright':
-                               $tags[$tag] = htmlspecialchars( $val );
-                               break;
-                       default:
-                               $tags[$tag] = $this->formatNum( $val );
-                               break;
-                       }
-               }
-
-               return $tags;
-       }
-
-       /**
-        * Convenience function for getFormattedData()
-        *
-        * @private
-        *
-        * @param $tag String: the tag name to pass on
-        * @param $val String: the value of the tag
-        * @param $arg String: an argument to pass ($1)
-        * @return string A wfMsg of "exif-$tag-$val" in lower case
-        */
-       function msg( $tag, $val, $arg = null ) {
-               global $wgContLang;
-
-               if ($val === '')
-                       $val = 'value';
-               return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg );
-       }
-
-       /**
-        * Format a number, convert numbers from fractions into floating point
-        * numbers
-        *
-        * @private
-        *
-        * @param $num Mixed: the value to format
-        * @return mixed A floating point number or whatever we were fed
-        */
-       function formatNum( $num ) {
-               global $wgLang;
-
-               $m = array();
-               if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) )
-                       return $wgLang->formatNum( $m[2] != 0 ? $m[1] / $m[2] : $num );
-               else
-                       return $wgLang->formatNum( $num );
-       }
-
-       /**
-        * Format a rational number, reducing fractions
-        *
-        * @private
-        *
-        * @param $num Mixed: the value to format
-        * @return mixed A floating point number or whatever we were fed
-        */
-       function formatFraction( $num ) {
-               $m = array();
-               if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) {
-                       $numerator = intval( $m[1] );
-                       $denominator = intval( $m[2] );
-                       $gcd = $this->gcd( $numerator, $denominator );
-                       if( $gcd != 0 ) {
-                               // 0 shouldn't happen! ;)
-                               return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd );
-                       }
-               }
-               return $this->formatNum( $num );
-       }
-
-       /**
-        * Calculate the greatest common divisor of two integers.
-        *
-        * @param $a Integer: FIXME
-        * @param $b Integer: FIXME
-        * @return int
-        * @private
-        */
-       function gcd( $a, $b ) {
-               /*
-                       // http://en.wikipedia.org/wiki/Euclidean_algorithm
-                       // Recursive form would be:
-                       if( $b == 0 )
-                               return $a;
-                       else
-                               return gcd( $b, $a % $b );
-               */
-               while( $b != 0 ) {
-                       $remainder = $a % $b;
-
-                       // tail recursion...
-                       $a = $b;
-                       $b = $remainder;
-               }
-               return $a;
-       }
-}
index c155529..1ac22a3 100644 (file)
@@ -268,7 +268,7 @@ class ImagePage extends Article {
         * FIXME: bad interface, see note on MediaHandler::formatMetadata().
         *
         * @param $metadata Array: the array containing the EXIF data
-        * @return String
+        * @return String The metadata table. This is treated as Wikitext (!)
         */
        protected function makeMetadataTable( $metadata ) {
                $r = "<div class=\"mw-imagepage-section-metadata\">";
index 7714305..002820d 100644 (file)
@@ -121,7 +121,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                        $gotOne = true;
 
                                        $fit = $this->addPageSubItem( $pageId,
-                                               self::getInfo( $img, $prop, $result, $finalThumbParams ) );
+                                               self::getInfo( $img, $prop, $result,
+                                                       $finalThumbParams, $params['metadataversion'] ) );
                                        if ( !$fit ) {
                                                if ( count( $pageIds[NS_IMAGE] ) == 1 ) {
                                                        // See the 'the user is screwed' comment above
@@ -150,7 +151,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                                break;
                                        }
                                        $fit = $this->addPageSubItem( $pageId,
-                                               self::getInfo( $oldie, $prop, $result, $finalThumbParams ) );
+                                               self::getInfo( $oldie, $prop, $result,
+                                                       $finalThumbParams, $params['metadataversion'] ) );
                                        if ( !$fit ) {
                                                if ( count( $pageIds[NS_IMAGE] ) == 1 ) {
                                                        $this->setContinueEnumParameter( 'start',
@@ -265,9 +267,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
         * @param $prop Array of properties to get (in the keys)
         * @param $result ApiResult object
         * @param $thumbParams Array containing 'width' and 'height' items, or null
+        * @param $version Version of image metadata (for things like jpeg which have different versions).
         * @return Array: result array
         */
-       static function getInfo( $file, $prop, $result, $thumbParams = null ) {
+       static function getInfo( $file, $prop, $result, $thumbParams = null, $version = 'latest' ) {
                $vals = array();
                // Timestamp is shown even if the file is revdelete'd in interface
                // so do same here.
@@ -376,8 +379,11 @@ class ApiQueryImageInfo extends ApiQueryBase {
                }
 
                if ( $meta ) {
-                       $metadata = $file->getMetadata();
-                       $vals['metadata'] = $metadata ? self::processMetaData( unserialize( $metadata ), $result ) : null;
+                       $metadata = unserialize( $file->getMetadata() );
+                       if ( $version !== 'latest' ) {
+                               $metadata = $file->convertMetadataVersion( $metadata, $version );
+                       }
+                       $vals['metadata'] = $metadata ? self::processMetaData( $metadata, $result ) : null;
                }
 
                if ( $mime ) {
@@ -463,6 +469,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                ApiBase::PARAM_TYPE => 'integer',
                                ApiBase::PARAM_DFLT => -1
                        ),
+                       'metadataversion' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_DFLT => '1',
+                       ),
                        'urlparam' => array(
                                ApiBase::PARAM_DFLT => '',
                                ApiBase::PARAM_TYPE => 'string',
@@ -540,6 +550,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        'limit' => 'How many image revisions to return',
                        'start' => 'Timestamp to start listing from',
                        'end' => 'Timestamp to stop listing at',
+                       'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.", 
+                                               "Defaults to '1' for backwards compatibility" ),
                        'continue' => 'If the query response includes a continue value, use it here to get another page of results'
                );
        }
index 49e7d1a..0e1ac94 100644 (file)
@@ -303,6 +303,26 @@ abstract class File {
         */
        public function getMetadata() { return false; }
 
+       /**
+       * get versioned metadata
+       *
+       * @param $metadata Mixed Array or String of (serialized) metadata
+       * @param $version integer version number.
+       * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
+       */
+       public function convertMetadataVersion($metadata, $version) {
+               $handler = $this->getHandler();
+               if (!is_array($metadata)) {
+                       //just to make the return type consistant
+                       $metadata = unserialize( $metadata ); 
+               }
+               if ( $handler ) {
+                       return $handler->convertMetadataVersion($metadata, $version);
+               } else {
+                       return $metadata;
+               }
+       }
+
        /**
         * Return the bit depth of the file
         * Overridden by LocalFile
index daf2149..f6ecad2 100644 (file)
@@ -38,7 +38,9 @@ class ForeignAPIFile extends File {
                $data = $repo->fetchImageQuery( array(
                         'titles' => 'File:' . $title->getDBKey(),
                         'iiprop' => self::getProps(),
-                        'prop' => 'imageinfo' ) );
+                        'prop' => 'imageinfo',
+                       'iimetadataversion' => mediaHandler::getMetadataVersion()
+                        ) );
 
                $info = $repo->getImageInfo( $data );
 
index 1f9184a..a6c247a 100644 (file)
@@ -336,6 +336,7 @@ class LocalFile extends File {
         * Upgrade a row if it needs it
         */
        function maybeUpgradeRow() {
+               global $wgUpdateCompatibleMetadata;
                if ( wfReadOnly() ) {
                        return;
                }
@@ -347,9 +348,14 @@ class LocalFile extends File {
                        $this->upgraded = true;
                } else {
                        $handler = $this->getHandler();
-                       if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
-                               $this->upgradeRow();
-                               $this->upgraded = true;
+                       if ( $handler ) {
+                               $validity = $handler->isMetadataValid( $this, $this->metadata );
+                               if ( $validity === MediaHandler::METADATA_BAD
+                                       || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata ) 
+                               ) {
+                                       $this->upgradeRow();
+                                       $this->upgraded = true;
+                               }
                        }
                }
        }
index ff2e56d..a9ebd3f 100644 (file)
@@ -670,8 +670,19 @@ class BitmapHandler extends ImageHandler {
                imagejpeg( $dst_image, $thumbPath, 95 );
        }
 
-
+       /**
+        * Its unclear if anything still uses this
+        * as jpeg is now in its own subclass.
+        *
+        * And really each media handler should use a
+        * different getMetadata, as the formats aren't
+        * all that similar and usually have different
+        * metadata needs.
+        *
+        * @deprecated
+        */
        function getMetadata( $image, $filename ) {
+               wfDeprected( __METHOD__ );
                global $wgShowEXIF;
                if ( $wgShowEXIF && file_exists( $filename ) ) {
                        $exif = new Exif( $filename );
diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php
new file mode 100644 (file)
index 0000000..80b9323
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+/**
+Class to deal with reconciling and extracting metadata from bitmap images.
+This is meant to comply with http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
+
+This sort of acts as an intermediary between MediaHandler::getMetadata
+and the various metadata extractors.
+
+@todo other image formats.
+*/
+class BitmapMetadataHandler {
+
+       private $metadata = Array();
+       private $metaPriority = Array(
+               20 => Array( 'other' ),
+               40 => Array( 'native' ),
+               60 => Array( 'iptc-good-hash', 'iptc-no-hash' ),
+               70 => Array( 'xmp-deprecated' ),
+               80 => Array( 'xmp-general' ),
+               90 => Array( 'xmp-exif' ),
+               100 => Array( 'iptc-bad-hash' ),
+               120 => Array( 'exif' ),
+       );
+       private $iptcType = 'iptc-no-hash';
+
+       /**
+       * This does the photoshop image resource app13 block
+       * of interest, IPTC-IIM metadata is stored here.
+       *
+       * Mostly just calls doPSIR and doIPTC
+       *
+       * @param String $app13 String containing app13 block from jpeg file
+       */
+       private function doApp13 ( $app13 ) {
+               $this->iptcType = JpegMetadataExtractor::doPSIR( $app13 );
+
+               $iptc = IPTC::parse( $app13 );
+               $this->addMetadata( $iptc, $this->iptcType );
+       }
+
+
+       /** get exif info using exif class.
+       * Basically what used to be in BitmapHandler::getMetadata().
+       * Just calls stuff in the Exif class.
+       */
+       function getExif ( $filename ) {
+               if ( file_exists( $filename ) ) {
+                       $exif = new Exif( $filename );
+                       $data = $exif->getFilteredData();
+                       if ( $data ) {
+                               $this->addMetadata( $data, 'exif' );
+                       }
+               }
+       }
+       /** Add misc metadata. Warning: atm if the metadata category
+       * doesn't have a priority, it will be silently discarded.
+       *
+       * @param Array $metaArray array of metadata values
+       * @param string $type type. defaults to other. if two things have the same type they're merged
+       */
+       function addMetadata ( $metaArray, $type = 'other' ) {
+               if ( isset( $this->metadata[$type] ) ) {
+                       /* merge with old data */
+                       $metaArray = $metaArray + $this->metadata[$type];
+               }
+
+               $this->metadata[$type] = $metaArray;
+       }
+
+       /**
+       * Merge together the various types of metadata
+       * the different types have different priorites,
+       * and are merged in order.
+       *
+       * This function is generally called by the media handlers' getMetadata()
+       *
+       * @return Array metadata array
+       */
+       function getMetadataArray () {
+               // this seems a bit ugly... This is all so its merged in right order
+               // based on the MWG recomendation.
+               $temp = Array();
+               krsort( $this->metaPriority );
+               foreach ( $this->metaPriority as $pri ) {
+                       foreach ( $pri as $type ) {
+                               if ( isset( $this->metadata[$type] ) ) {
+                                       // Do some special casing for multilingual values.
+                                       // Don't discard translations if also as a simple value.
+                                       foreach ( $this->metadata[$type] as $itemName => $item ) {
+                                               if ( is_array( $item ) && isset( $item['_type'] ) && $item['_type'] === 'lang' ) {
+                                                       if ( isset( $temp[$itemName] ) && !is_array( $temp[$itemName] ) ) {
+                                                               $default = $temp[$itemName];
+                                                               $temp[$itemName] = $item;
+                                                               $temp[$itemName]['x-default'] = $default;
+                                                               unset( $this->metadata[$type][$itemName] );
+                                                       }
+                                               }
+                                       }
+
+                                       $temp = $temp + $this->metadata[$type];
+                               }
+                       }
+               }
+               return $temp;
+       }
+
+       /** Main entry point for jpeg's.
+       *
+       * @param string $file filename (with full path)
+       * @return metadata result array.
+       * @throws MWException on invalid file.
+       */
+       static function Jpeg ( $filename ) {
+               $showXMP = function_exists( 'xml_parser_create_ns' );
+               $meta = new self();
+               $meta->getExif( $filename );
+               $seg = Array();
+               $seg = JpegMetadataExtractor::segmentSplitter( $filename );
+               if ( isset( $seg['COM'] ) && isset( $seg['COM'][0] ) ) {
+                       $meta->addMetadata( Array( 'JPEGFileComment' => $seg['COM'] ), 'native' );
+               }
+               if ( isset( $seg['PSIR'] ) ) {
+                       $meta->doApp13( $seg['PSIR'] );
+               }
+               if ( isset( $seg['XMP'] ) && $showXMP ) {
+                       $xmp = new XMPReader();
+                       $xmp->parse( $seg['XMP'] );
+                       foreach ( $seg['XMP_ext'] as $xmpExt ) {
+                               /* Support for extended xmp in jpeg files
+                                * is not well tested and a bit fragile.
+                                */
+                               $xmp->parseExtended( $xmpExt );
+
+                       }
+                       $res = $xmp->getResults();
+                       foreach ( $res as $type => $array ) {
+                               $meta->addMetadata( $array, $type );
+                       }
+               }
+               return $meta->getMetadataArray();
+       }
+       /** Entry point for png
+       * At some point in the future this might
+       * merge the png various tEXt chunks to that
+       * are interesting, but for now it only does XMP
+       *
+       * @param $filename String full path to file
+       * @return Array Array for storage in img_metadata.
+       */
+       static public function PNG ( $filename ) {
+               $showXMP = function_exists( 'xml_parser_create_ns' );
+
+               $meta = new self();
+               $array = PNGMetadataExtractor::getMetadata( $filename );
+               if ( isset( $array['text']['xmp']['x-default'] ) && $array['text']['xmp']['x-default'] !== '' && $showXMP ) {
+                       $xmp = new XMPReader();
+                       $xmp->parse( $array['text']['xmp']['x-default'] );
+                       $xmpRes = $xmp->getResults();
+                       foreach ( $xmpRes as $type => $xmpSection ) {
+                               $meta->addMetadata( $xmpSection, $type );
+                       }
+               }
+               unset( $array['text']['xmp'] );
+               $meta->addMetadata( $array['text'], 'native' );
+               unset( $array['text'] );
+               $array['metadata'] = $meta->getMetadataArray();
+               $array['metadata']['_MW_PNG_VERSION'] = PNGMetadataExtractor::VERSION;
+               return $array;
+       }
+
+       /** function for gif images.
+        *
+        * They don't really have native metadata, so just merges together
+        * XMP and image comment.
+        *
+        * @param $filename full path to file
+        * @return Array metadata array
+        */
+       static public function GIF ( $filename ) {
+
+               $meta = new self();
+               $baseArray = GIFMetadataExtractor::getMetadata( $filename );
+
+               if ( count( $baseArray['comment'] ) > 0 ) {
+                       $meta->addMetadata( array( 'GIFFileComment' => $baseArray['comment'] ), 'native' );
+               }
+
+               if ( $baseArray['xmp'] !== '' && function_exists( 'xml_parser_create_ns' ) ) {
+                       $xmp = new XMPReader();
+                       $xmp->parse( $baseArray['xmp'] );
+                       $xmpRes = $xmp->getResults();
+                       foreach ( $xmpRes as $type => $xmpSection ) {
+                               $meta->addMetadata( $xmpSection, $type );
+                       }
+
+               }
+
+               unset( $baseArray['comment'] );
+               unset( $baseArray['xmp'] );
+       
+               $baseArray['metadata'] = $meta->getMetadataArray();
+               $baseArray['metadata']['_MW_GIF_VERSION'] = GIFMetadataExtractor::VERSION;
+               return $baseArray;
+       }
+
+}
diff --git a/includes/media/Exif.php b/includes/media/Exif.php
new file mode 100644 (file)
index 0000000..42ebd5e
--- /dev/null
@@ -0,0 +1,786 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @ingroup Media
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason, 2009 Brent Garber
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
+ * @file
+ */
+
+/**
+ * Class to extract and validate Exif data from jpeg (and possibly tiff) files.
+ * @ingroup Media
+ */
+class Exif {
+
+       const BYTE      = 1;    //!< An 8-bit (1-byte) unsigned integer.
+       const ASCII     = 2;    //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.
+       const SHORT     = 3;    //!< A 16-bit (2-byte) unsigned integer.
+       const LONG      = 4;    //!< A 32-bit (4-byte) unsigned integer.
+       const RATIONAL  = 5;    //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator
+       const UNDEFINED = 7;    //!< An 8-bit byte that can take any value depending on the field definition
+       const SLONG     = 9;    //!< A 32-bit (4-byte) signed integer (2's complement notation),
+       const SRATIONAL = 10;   //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
+       const IGNORE    = -1;   // A fake value for things we don't want or don't support.
+
+       //@{
+       /* @var array
+        * @private
+        */
+
+       /**
+        * Exif tags grouped by category, the tagname itself is the key and the type
+        * is the value, in the case of more than one possible value type they are
+        * separated by commas.
+        */
+       var $mExifTags;
+
+       /**
+        * The raw Exif data returned by exif_read_data()
+        */
+       var $mRawExifData;
+
+       /**
+        * A Filtered version of $mRawExifData that has been pruned of invalid
+        * tags and tags that contain content they shouldn't contain according
+        * to the Exif specification
+        */
+       var $mFilteredExifData;
+
+       /**
+        * Filtered and formatted Exif data, see FormatMetadata::getFormattedData()
+        */
+       var $mFormattedExifData;
+
+       //@}
+
+       //@{
+       /* @var string
+        * @private
+        */
+
+       /**
+        * The file being processed
+        */
+       var $file;
+
+       /**
+        * The basename of the file being processed
+        */
+       var $basename;
+
+       /**
+        * The private log to log to, e.g. 'exif'
+        */
+       var $log = false;
+
+       //@}
+
+       /**
+        * Constructor
+        *
+        * @param $file String: filename.
+        * @fixme the following are broke:
+        * SubjectArea. Need to test the more obscure tags.
+        *
+        * DigitalZoomRatio = 0/0 is rejected. need to determine if that's valid.
+        * possibly should treat 0/0 = 0. need to read exif spec on that.
+        */
+       function __construct( $file ) {
+               /**
+                * Page numbers here refer to pages in the EXIF 2.2 standard
+                *
+                * Note, Exif::UNDEFINED is treated as a string, not as an array of bytes
+                * so don't put a count parameter for any UNDEFINED values.
+                *
+                * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification
+                */
+               $this->mExifTags = array(
+                       # TIFF Rev. 6.0 Attribute Information (p22)
+                       'IFD0' => array(
+                               # Tags relating to image structure
+                               'ImageWidth' => Exif::SHORT.','.Exif::LONG,             # Image width
+                               'ImageLength' => Exif::SHORT.','.Exif::LONG,            # Image height
+                               'BitsPerSample' => array( Exif::SHORT, 3 ),             # Number of bits per component
+                               # "When a primary image is JPEG compressed, this designation is not"
+                               # "necessary and is omitted." (p23)
+                               'Compression' => Exif::SHORT,                           # Compression scheme #p23
+                               'PhotometricInterpretation' => Exif::SHORT,             # Pixel composition #p23
+                               'Orientation' => Exif::SHORT,                           # Orientation of image #p24
+                               'SamplesPerPixel' => Exif::SHORT,                       # Number of components
+                               'PlanarConfiguration' => Exif::SHORT,                   # Image data arrangement #p24
+                               'YCbCrSubSampling' => array( Exif::SHORT, 2),           # Subsampling ratio of Y to C #p24
+                               'YCbCrPositioning' => Exif::SHORT,                      # Y and C positioning #p24-25
+                               'XResolution' => Exif::RATIONAL,                        # Image resolution in width direction
+                               'YResolution' => Exif::RATIONAL,                        # Image resolution in height direction
+                               'ResolutionUnit' => Exif::SHORT,                        # Unit of X and Y resolution #(p26)
+
+                               # Tags relating to recording offset
+                               'StripOffsets' => Exif::SHORT.','.Exif::LONG,                   # Image data location
+                               'RowsPerStrip' => Exif::SHORT.','.Exif::LONG,                   # Number of rows per strip
+                               'StripByteCounts' => Exif::SHORT.','.Exif::LONG,                # Bytes per compressed strip
+                               'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG,          # Offset to JPEG SOI
+                               'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG,    # Bytes of JPEG data
+
+                               # Tags relating to image data characteristics
+                               'TransferFunction' => Exif::IGNORE,                     # Transfer function
+                               'WhitePoint' => array( Exif::RATIONAL, 2),              # White point chromaticity
+                               'PrimaryChromaticities' => array( Exif::RATIONAL, 6),   # Chromaticities of primarities
+                               'YCbCrCoefficients' => array( Exif::RATIONAL, 3),       # Color space transformation matrix coefficients #p27
+                               'ReferenceBlackWhite' => array( Exif::RATIONAL, 6),     # Pair of black and white reference values
+
+                               # Other tags
+                               'DateTime' => Exif::ASCII,                              # File change date and time
+                               'ImageDescription' => Exif::ASCII,                      # Image title
+                               'Make' => Exif::ASCII,                                  # Image input equipment manufacturer
+                               'Model' => Exif::ASCII,                                 # Image input equipment model
+                               'Software' => Exif::ASCII,                              # Software used
+                               'Artist' => Exif::ASCII,                                # Person who created the image
+                               'Copyright' => Exif::ASCII,                             # Copyright holder
+                       ),
+
+                       # Exif IFD Attribute Information (p30-31)
+                       'EXIF' => array(
+                               # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance
+                               # to the EXIF 2.1 AND 2.2 standards
+                               'ExifVersion' =>  Exif::UNDEFINED,                      # Exif version
+                               'FlashPixVersion' => Exif::UNDEFINED,                   # Supported Flashpix version #p32
+
+                               # Tags relating to Image Data Characteristics
+                               'ColorSpace' => Exif::SHORT,                            # Color space information #p32
+
+                               # Tags relating to image configuration
+                               'ComponentsConfiguration' => Exif::UNDEFINED,                   # Meaning of each component #p33
+                               'CompressedBitsPerPixel' => Exif::RATIONAL,                     # Image compression mode
+                               'PixelYDimension' => Exif::SHORT.','.Exif::LONG,                # Valid image width
+                               'PixelXDimension' => Exif::SHORT.','.Exif::LONG,                # Valid image height
+
+                               # Tags relating to related user information
+                               'MakerNote' => Exif::IGNORE,                            # Manufacturer notes
+                               'UserComment' => Exif::UNDEFINED,                       # User comments #p34
+
+                               # Tags relating to related file information
+                               'RelatedSoundFile' => Exif::ASCII,                      # Related audio file
+
+                               # Tags relating to date and time
+                               'DateTimeOriginal' => Exif::ASCII,                      # Date and time of original data generation #p36
+                               'DateTimeDigitized' => Exif::ASCII,                     # Date and time of original data generation
+                               'SubSecTime' => Exif::ASCII,                            # DateTime subseconds
+                               'SubSecTimeOriginal' => Exif::ASCII,                    # DateTimeOriginal subseconds
+                               'SubSecTimeDigitized' => Exif::ASCII,                   # DateTimeDigitized subseconds
+
+                               # Tags relating to picture-taking conditions (p31)
+                               'ExposureTime' => Exif::RATIONAL,                       # Exposure time
+                               'FNumber' => Exif::RATIONAL,                            # F Number
+                               'ExposureProgram' => Exif::SHORT,                       # Exposure Program #p38
+                               'SpectralSensitivity' => Exif::ASCII,                   # Spectral sensitivity
+                               'ISOSpeedRatings' => Exif::SHORT,                       # ISO speed rating
+                               'OECF' => Exif::IGNORE,
+                               # Optoelectronic conversion factor. Note: We don't have support for this atm.
+                               'ShutterSpeedValue' => Exif::SRATIONAL,                 # Shutter speed
+                               'ApertureValue' => Exif::RATIONAL,                      # Aperture
+                               'BrightnessValue' => Exif::SRATIONAL,                   # Brightness
+                               'ExposureBiasValue' => Exif::SRATIONAL,                 # Exposure bias
+                               'MaxApertureValue' => Exif::RATIONAL,                   # Maximum land aperture
+                               'SubjectDistance' => Exif::RATIONAL,                    # Subject distance
+                               'MeteringMode' => Exif::SHORT,                          # Metering mode #p40
+                               'LightSource' => Exif::SHORT,                           # Light source #p40-41
+                               'Flash' => Exif::SHORT,                                 # Flash #p41-42
+                               'FocalLength' => Exif::RATIONAL,                        # Lens focal length
+                               'SubjectArea' => array( Exif::SHORT, 4 ),               # Subject area
+                               'FlashEnergy' => Exif::RATIONAL,                        # Flash energy
+                               'SpatialFrequencyResponse' => Exif::IGNORE,             # Spatial frequency response. Not supported atm.
+                               'FocalPlaneXResolution' => Exif::RATIONAL,              # Focal plane X resolution
+                               'FocalPlaneYResolution' => Exif::RATIONAL,              # Focal plane Y resolution
+                               'FocalPlaneResolutionUnit' => Exif::SHORT,              # Focal plane resolution unit #p46
+                               'SubjectLocation' => array( Exif::SHORT, 2),            # Subject location
+                               'ExposureIndex' => Exif::RATIONAL,                      # Exposure index
+                               'SensingMethod' => Exif::SHORT,                         # Sensing method #p46
+                               'FileSource' => Exif::UNDEFINED,                        # File source #p47
+                               'SceneType' => Exif::UNDEFINED,                         # Scene type #p47
+                               'CFAPattern' => Exif::IGNORE,                           # CFA pattern. not supported atm.
+                               'CustomRendered' => Exif::SHORT,                        # Custom image processing #p48
+                               'ExposureMode' => Exif::SHORT,                          # Exposure mode #p48
+                               'WhiteBalance' => Exif::SHORT,                          # White Balance #p49
+                               'DigitalZoomRatio' => Exif::RATIONAL,                   # Digital zoom ration
+                               'FocalLengthIn35mmFilm' => Exif::SHORT,                 # Focal length in 35 mm film
+                               'SceneCaptureType' => Exif::SHORT,                      # Scene capture type #p49
+                               'GainControl' => Exif::SHORT,                           # Scene control #p49-50
+                               'Contrast' => Exif::SHORT,                              # Contrast #p50
+                               'Saturation' => Exif::SHORT,                            # Saturation #p50
+                               'Sharpness' => Exif::SHORT,                             # Sharpness #p50
+                               'DeviceSettingDescription' => Exif::IGNORE,
+                               # Device settings description. This could maybe be supported. Need to find an
+                               # example file that uses this to see if it has stuff of interest in it.
+                               'SubjectDistanceRange' => Exif::SHORT,                  # Subject distance range #p51
+
+                               'ImageUniqueID' => Exif::ASCII,                         # Unique image ID
+                       ),
+
+                       # GPS Attribute Information (p52)
+                       'GPS' => array(
+                               'GPSVersion' => Exif::UNDEFINED,
+                               # Should be an array of 4 Exif::BYTE's. However php treats it as an undefined
+                               # Note exif standard calls this GPSVersionID, but php doesn't like the id suffix
+                               'GPSLatitudeRef' => Exif::ASCII,                        # North or South Latitude #p52-53
+                               'GPSLatitude' => array( Exif::RATIONAL, 3 ),            # Latitude
+                               'GPSLongitudeRef' => Exif::ASCII,                       # East or West Longitude #p53
+                               'GPSLongitude' => array( Exif::RATIONAL, 3),            # Longitude
+                               'GPSAltitudeRef' => Exif::UNDEFINED,
+                               # Altitude reference. Note, the exif standard says this should be an EXIF::Byte,
+                               # but php seems to disagree.
+                               'GPSAltitude' => Exif::RATIONAL,                        # Altitude
+                               'GPSTimeStamp' => array( Exif::RATIONAL, 3),            # GPS time (atomic clock)
+                               'GPSSatellites' => Exif::ASCII,                         # Satellites used for measurement
+                               'GPSStatus' => Exif::ASCII,                             # Receiver status #p54
+                               'GPSMeasureMode' => Exif::ASCII,                        # Measurement mode #p54-55
+                               'GPSDOP' => Exif::RATIONAL,                             # Measurement precision
+                               'GPSSpeedRef' => Exif::ASCII,                           # Speed unit #p55
+                               'GPSSpeed' => Exif::RATIONAL,                           # Speed of GPS receiver
+                               'GPSTrackRef' => Exif::ASCII,                           # Reference for direction of movement #p55
+                               'GPSTrack' => Exif::RATIONAL,                           # Direction of movement
+                               'GPSImgDirectionRef' => Exif::ASCII,                    # Reference for direction of image #p56
+                               'GPSImgDirection' => Exif::RATIONAL,                    # Direction of image
+                               'GPSMapDatum' => Exif::ASCII,                           # Geodetic survey data used
+                               'GPSDestLatitudeRef' => Exif::ASCII,                    # Reference for latitude of destination #p56
+                               'GPSDestLatitude' => array( Exif::RATIONAL, 3 ),        # Latitude destination
+                               'GPSDestLongitudeRef' => Exif::ASCII,                   # Reference for longitude of destination #p57
+                               'GPSDestLongitude' => array( Exif::RATIONAL, 3 ),       # Longitude of destination
+                               'GPSDestBearingRef' => Exif::ASCII,                     # Reference for bearing of destination #p57
+                               'GPSDestBearing' => Exif::RATIONAL,                     # Bearing of destination
+                               'GPSDestDistanceRef' => Exif::ASCII,                    # Reference for distance to destination #p57-58
+                               'GPSDestDistance' => Exif::RATIONAL,                    # Distance to destination
+                               'GPSProcessingMethod' => Exif::UNDEFINED,               # Name of GPS processing method
+                               'GPSAreaInformation' => Exif::UNDEFINED,                # Name of GPS area
+                               'GPSDateStamp' => Exif::ASCII,                          # GPS date
+                               'GPSDifferential' => Exif::SHORT,                       # GPS differential correction
+                       ),
+               );
+
+               $this->file = $file;
+               $this->basename = wfBaseName( $this->file );
+
+               $this->debugFile( $this->basename, __FUNCTION__, true );
+               if( function_exists( 'exif_read_data' ) ) {
+                       wfSuppressWarnings();
+                       $data = exif_read_data( $this->file, 0, true );
+                       wfRestoreWarnings();
+               } else {
+                       throw new MWException( "Internal error: exif_read_data not present. \$wgShowEXIF may be incorrectly set or not checked by an extension." );
+               }
+               /**
+                * exif_read_data() will return false on invalid input, such as
+                * when somebody uploads a file called something.jpeg
+                * containing random gibberish.
+                */
+               $this->mRawExifData = $data ? $data : array();
+               $this->makeFilteredData();
+               $this->collapseData();
+               $this->debugFile( __FUNCTION__, false );
+       }
+
+       /**
+        * Make $this->mFilteredExifData
+        */
+       function makeFilteredData() {
+               $this->mFilteredExifData = Array();
+
+               foreach ( array_keys( $this->mRawExifData ) as $section ) {
+                       if ( !in_array( $section, array_keys( $this->mExifTags ) ) ) {
+                               $this->debug( $section , __FUNCTION__, "'$section' is not a valid Exif section" );
+                               continue;
+                       }
+
+                       foreach ( array_keys( $this->mRawExifData[$section] ) as $tag ) {
+                               if ( !in_array( $tag, array_keys( $this->mExifTags[$section] ) ) ) {
+                                       $this->debug( $tag, __FUNCTION__, "'$tag' is not a valid tag in '$section'" );
+                                       continue;
+                               }
+
+                               $this->mFilteredExifData[$tag] = $this->mRawExifData[$section][$tag];
+                               // This is ok, as the tags in the different sections do not conflict.
+                               // except in computed and thumbnail section, which we don't use.
+
+                               $value = $this->mRawExifData[$section][$tag];
+                               if ( !$this->validate( $section, $tag, $value ) ) {
+                                       $this->debug( $value, __FUNCTION__, "'$tag' contained invalid data" );
+                                       unset( $this->mFilteredExifData[$tag] );
+                               }
+                       }
+               }
+       }
+
+       /**
+       * Collapse some fields together.
+       * This converts some fields from exif form, to a more friendly form.
+       * For example GPS latitude to a single number.
+       *
+       * The rationale behind this is that we're storing data, not presenting to the user
+       * For example a longitude is a single number describing how far away you are from
+       * the prime meridian. Well it might be nice to split it up into minutes and seconds
+       * for the user, it doesn't really make sense to split a single number into 4 parts
+       * for storage. (degrees, minutes, second, direction vs single floating point number).
+       *
+       * Other things this might do (not really sure if they make sense or not):
+       * Dates -> mediawiki date format.
+       * convert values that can be in different units to be in one standardized unit.
+       *
+       * As an alternative approach, some of this could be done in the validate phase
+       * if we make up our own types like Exif::DATE.
+       */
+       function collapseData( ) {
+
+               $this->exifGPStoNumber( 'GPSLatitude' );
+               $this->exifGPStoNumber( 'GPSDestLatitude' );
+               $this->exifGPStoNumber( 'GPSLongitude' );
+               $this->exifGPStoNumber( 'GPSDestLongitude' );
+
+               if ( isset( $this->mFilteredExifData['GPSAltitude'] ) && isset( $this->mFilteredExifData['GPSAltitudeRef'] ) ) {
+                       if ( $this->mFilteredExifData['GPSAltitudeRef'] === "\1" ) {
+                               $this->mFilteredExifData['GPSAltitude'] *= - 1;
+                       }
+                       unset( $this->mFilteredExifData['GPSAltitudeRef'] );
+               }
+
+               $this->exifPropToOrd( 'FileSource' );
+               $this->exifPropToOrd( 'SceneType' );
+
+               $this->charCodeString( 'UserComment' );
+               $this->charCodeString( 'GPSProcessingMethod');
+               $this->charCodeString( 'GPSAreaInformation' );
+               
+               //ComponentsConfiguration should really be an array instead of a string...
+               //This turns a string of binary numbers into an array of numbers.
+
+               if ( isset ( $this->mFilteredExifData['ComponentsConfiguration'] ) ) {
+                       $val = $this->mFilteredExifData['ComponentsConfiguration'];
+                       $ccVals = array();
+                       for ($i = 0; $i < strlen($val); $i++) {
+                               $ccVals[$i] = ord( substr($val, $i, 1) );
+                       }
+                       $ccVals['_type'] = 'ol'; //this is for formatting later.
+                       $this->mFilteredExifData['ComponentsConfiguration'] = $ccVals;
+               }
+       
+               //GPSVersion(ID) is treated as the wrong type by php exif support.
+               //Go through each byte turning it into a version string.
+               //For example: "\x02\x02\x00\x00" -> "2.2.0.0"
+
+               //Also change exif tag name from GPSVersion (what php exif thinks it is)
+               //to GPSVersionID (what the exif standard thinks it is).
+
+               if ( isset ( $this->mFilteredExifData['GPSVersion'] ) ) {
+                       $val = $this->mFilteredExifData['GPSVersion'];
+                       $newVal = '';
+                       for ($i = 0; $i < strlen($val); $i++) {
+                               if ( $i !== 0 ) {
+                                       $newVal .= '.';
+                               }
+                               $newVal .= ord( substr($val, $i, 1) );
+                       }
+                       $this->mFilteredExifData['GPSVersionID'] = $newVal;
+                       unset( $this->mFilteredExifData['GPSVersion'] );
+               }
+
+       }
+       /**
+       * Do userComment tags and similar. See pg. 34 of exif standard.
+       * basically first 8 bytes is charset, rest is value.
+       * This has not been tested on any shift-JIS strings.
+       * @param $prop String prop name.
+       */
+       private function charCodeString ( $prop ) {
+               if ( isset( $this->mFilteredExifData[$prop] ) ) {
+
+                       if ( strlen($this->mFilteredExifData[$prop]) <= 8 ) {
+                               //invalid. Must be at least 9 bytes long.
+
+                               $this->debug( $this->mFilteredExifData[$prop] , __FUNCTION__, false );
+                               unset($this->mFilteredExifData[$prop]);
+                               return;
+                       }
+
+                       $charCode = substr( $this->mFilteredExifData[$prop], 0, 8);
+                       $val = substr( $this->mFilteredExifData[$prop], 8);
+                       
+                       
+                       switch ($charCode) {
+                               case "\x4A\x49\x53\x00\x00\x00\x00\x00":
+                                       //JIS
+                                       $charset = "Shift-JIS";
+                                       break;
+                               case "UNICODE\x00":
+                                       $charset = "UTF-16";
+                                       break;
+                               default: //ascii or undefined.
+                                       $charset = "";
+                                       break;
+                       }
+                       // This could possibly check to see if iconv is really installed
+                       // or if we're using the compatibility wrapper in globalFunctions.php
+                       if ($charset) {
+                               $val = iconv($charset, 'UTF-8//IGNORE', $val);
+                       } else {
+                               // if valid utf-8, assume that, otherwise assume windows-1252
+                               $valCopy = $val;
+                               UtfNormal::quickIsNFCVerify( $valCopy ); //validates $valCopy.
+                               if ( $valCopy !== $val ) {
+                                       $val = iconv('Windows-1252', 'UTF-8//IGNORE', $val);
+                               }
+                       }
+                       
+                       //trim and check to make sure not only whitespace.
+                       $val = trim($val);
+                       if ( strlen( $val ) === 0 ) {
+                               //only whitespace.
+                               $this->debug( $this->mFilteredExifData[$prop] , __FUNCTION__, "$prop: Is only whitespace" );
+                               unset($this->mFilteredExifData[$prop]);
+                               return;
+                       }
+
+                       //all's good.
+                       $this->mFilteredExifData[$prop] = $val;
+               }
+       }
+       /**
+       * Convert an Exif::UNDEFINED from a raw binary string
+       * to its value. This is sometimes needed depending on
+       * the type of UNDEFINED field
+       * @param $prop String name of property
+       */
+       private function exifPropToOrd ( $prop ) {
+               if ( isset( $this->mFilteredExifData[$prop] ) ) {
+                       $this->mFilteredExifData[$prop] = ord( $this->mFilteredExifData[$prop] );
+               }
+       }
+       /**
+       * Convert gps in exif form to a single floating point number
+       * for example 10 degress 20`40`` S -> -10.34444
+       * @param String $prop a gps coordinate exif tag name (like GPSLongitude)
+       */
+       private function exifGPStoNumber ( $prop ) {
+               $loc =& $this->mFilteredExifData[$prop];
+               $dir =& $this->mFilteredExifData[$prop . 'Ref'];
+               $res = false;
+
+               if ( isset( $loc ) && isset( $dir ) && ( $dir === 'N' || $dir === 'S' || $dir === 'E' || $dir === 'W' ) ) {
+                       list( $num, $denom ) = explode( '/', $loc[0] );
+                       $res = $num / $denom;
+                       list( $num, $denom ) = explode( '/', $loc[1] );
+                       $res += ( $num / $denom ) * ( 1 / 60 );
+                       list( $num, $denom ) = explode( '/', $loc[2] );
+                       $res += ( $num / $denom ) * ( 1 / 3600 );
+
+                       if ( $dir === 'S' || $dir === 'W' ) {
+                               $res *= - 1; // make negative
+                       }
+               }
+
+               // update the exif records.
+
+               if ( $res !== false ) { // using !== as $res could potentially be 0
+                       $this->mFilteredExifData[$prop] = $res;
+                       unset( $this->mFilteredExifData[$prop . 'Ref'] );
+               } else { // if invalid
+                       unset( $this->mFilteredExifData[$prop] );
+                       unset( $this->mFilteredExifData[$prop . 'Ref'] );
+               }
+       }
+
+       /**
+        * Use FormatMetadata to create formatted values for display to user
+        * (is this ever used?)
+        */
+       private function makeFormattedData( ) {
+               $this->mFormattedExifData = FormatMetadata::getFormattedData();
+       }
+       /**#@-*/
+
+       /**#@+
+        * @return array
+        */
+       /**
+        * Get $this->mRawExifData
+        */
+       function getData() {
+               return $this->mRawExifData;
+       }
+
+       /**
+        * Get $this->mFilteredExifData
+        */
+       function getFilteredData() {
+               return $this->mFilteredExifData;
+       }
+
+       /**
+        * Get $this->mFormattedExifData
+        *
+        * This returns the data for display to user.
+        * Its unclear if this is ever used.
+        */
+       function getFormattedData() {
+               if (!$this->mFormattedExifData) {
+                       $this->makeFormattedData();
+               }
+               return $this->mFormattedExifData;
+       }
+       /**#@-*/
+
+       /**
+        * The version of the output format
+        *
+        * Before the actual metadata information is saved in the database we
+        * strip some of it since we don't want to save things like thumbnails
+        * which usually accompany Exif data. This value gets saved in the
+        * database along with the actual Exif data, and if the version in the
+        * database doesn't equal the value returned by this function the Exif
+        * data is regenerated.
+        *
+        * @return int
+        */
+       public static function version() {
+               return 2; // We don't need no bloddy constants!
+       }
+
+       /**#@+
+        * Validates if a tag value is of the type it should be according to the Exif spec
+        *
+        * @private
+        *
+        * @param $in Mixed: the input value to check
+        * @return bool
+        */
+       private function isByte( $in ) {
+               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) {
+                       $this->debug( $in, __FUNCTION__, true );
+                       return true;
+               } else {
+                       $this->debug( $in, __FUNCTION__, false );
+                       return false;
+               }
+       }
+
+       private function isASCII( $in ) {
+               if ( is_array( $in ) ) {
+                       return false;
+               }
+
+               if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) {
+                       $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' );
+                       return false;
+               }
+
+               if ( preg_match( '/^\s*$/', $in ) ) {
+                       $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' );
+                       return false;
+               }
+
+               return true;
+       }
+
+       private function isShort( $in ) {
+               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) {
+                       $this->debug( $in, __FUNCTION__, true );
+                       return true;
+               } else {
+                       $this->debug( $in, __FUNCTION__, false );
+                       return false;
+               }
+       }
+
+       private function isLong( $in ) {
+               if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) {
+                       $this->debug( $in, __FUNCTION__, true );
+                       return true;
+               } else {
+                       $this->debug( $in, __FUNCTION__, false );
+                       return false;
+               }
+       }
+
+       private function isRational( $in ) {
+               $m = array();
+               if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
+                       return $this->isLong( $m[1] ) && $this->isLong( $m[2] );
+               } else {
+                       $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
+                       return false;
+               }
+       }
+
+       private function isUndefined( $in ) {
+
+               $this->debug( $in, __FUNCTION__, true );
+               return true;
+
+               /* Exif::UNDEFINED means string of bytes
+               so this validation does not make sense.
+               comment out for now.
+               if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion
+                       $this->debug( $in, __FUNCTION__, true );
+                       return true;
+               } else {
+                       $this->debug( $in, __FUNCTION__, false );
+                       return false;
+               }
+               */
+       }
+
+       private function isSlong( $in ) {
+               if ( $this->isLong( abs( $in ) ) ) {
+                       $this->debug( $in, __FUNCTION__, true );
+                       return true;
+               } else {
+                       $this->debug( $in, __FUNCTION__, false );
+                       return false;
+               }
+       }
+
+       private function isSrational( $in ) {
+               $m = array();
+               if ( !is_array( $in ) && preg_match( '/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero
+                       return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] );
+               } else {
+                       $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' );
+                       return false;
+               }
+       }
+       /**#@-*/
+
+       /**
+        * Validates if a tag has a legal value according to the Exif spec
+        *
+        * @private
+        * @param $section String: section where tag is located.
+        * @param $tag String: the tag to check.
+        * @param $val Mixed: the value of the tag.
+        * @param $recursive Boolean: true if called recursively for array types.
+        * @return bool
+        */
+       private function validate( $section, $tag, $val, $recursive = false ) {
+               $debug = "tag is '$tag'";
+               $etype = $this->mExifTags[$section][$tag];
+               $ecount = 1;
+               if( is_array( $etype ) ) {
+                       list( $etype, $ecount ) = $etype;
+                       if ( $recursive )
+                               $ecount = 1; // checking individual elements
+               }
+               $count = count( $val );
+               if( $ecount != $count ) {
+                       $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" );
+                       return false;
+               }
+               if( $count > 1 ) {
+                       foreach( $val as $v ) { 
+                               if( !$this->validate( $section, $tag, $v, true ) ) {
+                                       return false; 
+                               } 
+                       }
+                       return true;
+               }
+               // Does not work if not typecast
+               switch( (string)$etype ) {
+                       case (string)Exif::BYTE:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isByte( $val );
+                       case (string)Exif::ASCII:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isASCII( $val );
+                       case (string)Exif::SHORT:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isShort( $val );
+                       case (string)Exif::LONG:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isLong( $val );
+                       case (string)Exif::RATIONAL:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isRational( $val );
+                       case (string)Exif::UNDEFINED:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isUndefined( $val );
+                       case (string)Exif::SLONG:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isSlong( $val );
+                       case (string)Exif::SRATIONAL:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isSrational( $val );
+                       case (string)Exif::SHORT.','.Exif::LONG:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return $this->isShort( $val ) || $this->isLong( $val );
+                       case (string)Exif::IGNORE:
+                               $this->debug( $val, __FUNCTION__, $debug );
+                               return false;
+                       default:
+                               $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" );
+                               return false;
+               }
+       }
+
+       /**
+        * Convenience function for debugging output
+        *
+        * @private
+        *
+        * @param $in Mixed:
+        * @param $fname String:
+        * @param $action Mixed: , default NULL.
+        */
+       private function debug( $in, $fname, $action = null ) {
+               if ( !$this->log ) {
+                       return;
+               }
+               $type = gettype( $in );
+               $class = ucfirst( __CLASS__ );
+               if ( $type === 'array' )
+                       $in = print_r( $in, true );
+
+               if ( $action === true )
+                       wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n");
+               elseif ( $action === false )
+                       wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n");
+               elseif ( $action === null )
+                       wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n");
+               else
+                       wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n");
+       }
+
+       /**
+        * Convenience function for debugging output
+        *
+        * @private
+        *
+        * @param $fname String: the name of the function calling this function
+        * @param $io Boolean: Specify whether we're beginning or ending
+        */
+       private function debugFile( $fname, $io ) {
+               if ( !$this->log ) {
+                       return;
+               }
+               $class = ucfirst( __CLASS__ );
+               if ( $io ) {
+                       wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" );
+               } else {
+                       wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" );
+               }
+       }
+
+}
+
diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php
new file mode 100644 (file)
index 0000000..ca73f62
--- /dev/null
@@ -0,0 +1,1352 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @ingroup Media
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason, 2009 Brent Garber, 2010 Brian Wolff
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification
+ * @file
+ */
+
+
+/**
+ * Format Image metadata values into a human readable form.
+ *
+ * Note lots of these messages use the prefix 'exif' even though
+ * they may not be exif properties. For example 'exif-ImageDescription'
+ * can be the Exif ImageDescription, or it could be the iptc-iim caption
+ * property, or it could be the xmp dc:description property. This
+ * is because these messages should be independent of how the data is
+ * stored, sine the user doesn't care if the description is stored in xmp,
+ * exif, etc only that its a description. (Additionally many of these properties
+ * are merged together following the MWG standard, such that for example,
+ * exif properties override XMP properties that mean the same thing if
+ * there is a conflict).
+ *
+ * It should perhaps use a prefix like 'metadata' instead, but there
+ * is already a large number of messages using the 'exif' prefix.
+ *
+ * @ingroup Media
+ */
+class FormatMetadata {
+
+       /**
+        * Numbers given by Exif user agents are often magical, that is they
+        * should be replaced by a detailed explanation depending on their
+        * value which most of the time are plain integers. This function
+        * formats Exif (and other metadata) values into human readable form.
+        *
+        * @param $tags Array: the Exif data to format ( as returned by
+        *                    Exif::getFilteredData() or BitmapMetadataHandler )
+        * @return array
+        */
+       public static function getFormattedData( $tags ) {
+               global $wgLang;
+
+               $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3;
+               unset( $tags['ResolutionUnit'] );
+
+               foreach ( $tags as $tag => &$vals ) {
+
+                       // This seems ugly to wrap non-array's in an array just to unwrap again,
+                       // especially when most of the time it is not an array
+                       if ( !is_array( $tags[$tag] ) ) {
+                               $vals = Array( $vals );
+                       }
+
+                       // _type is a special value to say what array type
+                       if ( isset( $tags[$tag]['_type'] ) ) {
+                               $type = $tags[$tag]['_type'];
+                               unset( $vals['_type'] );
+                       } else {
+                               $type = 'ul'; // default unordered list.
+                       }
+
+                       //This is done differently as the tag is an array.
+                       if ($tag == 'GPSTimeStamp' && count($vals) === 3) {
+                               //hour min sec array
+
+                               $h = explode('/', $vals[0]);
+                               $m = explode('/', $vals[1]);
+                               $s = explode('/', $vals[2]);
+
+                               // this should already be validated
+                               // when loaded from file, but it could
+                               // come from a foreign repo, so be
+                               // paranoid.
+                               if ( !isset($h[1])
+                                       || !isset($m[1])
+                                       || !isset($s[1])
+                                       || $h[1] == 0
+                                       || $m[1] == 0
+                                       || $s[1] == 0
+                               ) {
+                                       continue;
+                               }
+                               $tags[$tag] = intval( $h[0] / $h[1] )
+                                       . ':' . str_pad( intval( $m[0] / $m[1] ), 2, '0', STR_PAD_LEFT )
+                                       . ':' . str_pad( intval( $s[0] / $s[1] ), 2, '0', STR_PAD_LEFT );
+
+                               $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] );
+                               // the 1971:01:01 is just a placeholder, and not shown to user.
+                               if ( $time ) {
+                                       $tags[$tag] = $wgLang->time( $time );
+                               }
+                               continue;
+                       }
+
+                       // The contact info is a multi-valued field
+                       // instead of the other props which are single
+                       // valued (mostly) so handle as a special case.
+                       if ( $tag === 'Contact' ) {
+                               $vals = self::collapseContactInfo( $vals );
+                               continue;
+                       }
+
+                       foreach ( $vals as &$val ) {
+
+                               switch( $tag ) {
+                               case 'Compression':
+                                       switch( $val ) {
+                                       case 1: case 6:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'PhotometricInterpretation':
+                                       switch( $val ) {
+                                       case 2: case 6:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'Orientation':
+                                       switch( $val ) {
+                                       case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'PlanarConfiguration':
+                                       switch( $val ) {
+                                       case 1: case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               // TODO: YCbCrSubSampling
+                               case 'YCbCrPositioning':
+                                       switch ( $val ) {
+                                       case 1:
+                                       case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'XResolution':
+                               case 'YResolution':
+                                       switch( $resolutionunit ) {
+                                               case 2:
+                                                       $val = self::msg( 'XYResolution', 'i', self::formatNum( $val ) );
+                                                       break;
+                                               case 3:
+                                                       $val = self::msg( 'XYResolution', 'c', self::formatNum( $val ) );
+                                                       break;
+                                               default:
+                                                       $val = $val;
+                                                       break;
+                                       }
+                                       break;
+
+                               // TODO: YCbCrCoefficients  #p27 (see annex E)
+                               case 'ExifVersion': case 'FlashpixVersion':
+                                       $val = "$val" / 100;
+                                       break;
+
+                               case 'ColorSpace':
+                                       switch( $val ) {
+                                       case 1: case 65535:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'ComponentsConfiguration':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3: case 4: case 5: case 6:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'DateTime':
+                               case 'DateTimeOriginal':
+                               case 'DateTimeDigitized':
+                               case 'DateTimeReleased':
+                               case 'DateTimeExpires':
+                               case 'GPSDateStamp':
+                               case 'dc-date':
+                               case 'DateTimeMetadata':
+                                       if ( $val == '0000:00:00 00:00:00' || $val == '    :  :     :  :  ' ) {
+                                               $val = wfMsg( 'exif-unknowndate' );
+                                       } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) {
+                                               $time = wfTimestamp( TS_MW, $val );
+                                               if ( $time ) {
+                                                       $val = $wgLang->timeanddate( $time );
+                                               }
+                                       } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d)$/D', $val ) ) {
+                                               // If only the date but not the time is filled in.
+                                               $time = wfTimestamp( TS_MW, substr( $val, 0, 4 )
+                                                       . substr( $val, 5, 2 )
+                                                       . substr( $val, 8, 2 )
+                                                       . '000000' );
+                                               if ( $time ) {
+                                                       $val = $wgLang->date( $time );
+                                               }
+                                       }
+                                       // else it will just output $val without formatting it.
+                                       break;
+
+                               case 'ExposureProgram':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'SubjectDistance':
+                                       $val = self::msg( $tag, '', self::formatNum( $val ) );
+                                       break;
+
+                               case 'MeteringMode':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'LightSource':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11:
+                                       case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20:
+                                       case 21: case 22: case 23: case 24: case 255:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'Flash':
+                                       $flashDecode = array(
+                                               'fired'    => $val & bindec( '00000001' ),
+                                               'return'   => ( $val & bindec( '00000110' ) ) >> 1,
+                                               'mode'     => ( $val & bindec( '00011000' ) ) >> 3,
+                                               'function' => ( $val & bindec( '00100000' ) ) >> 5,
+                                               'redeye'   => ( $val & bindec( '01000000' ) ) >> 6,
+//                                             'reserved' => ($val & bindec( '10000000' )) >> 7,
+                                       );
+       
+                                       # We do not need to handle unknown values since all are used.
+                                       foreach ( $flashDecode as $subTag => $subValue ) {
+                                               # We do not need any message for zeroed values.
+                                               if ( $subTag != 'fired' && $subValue == 0 ) {
+                                                       continue;
+                                               }
+                                               $fullTag = $tag . '-' . $subTag ;
+                                               $flashMsgs[] = self::msg( $fullTag, $subValue );
+                                       }
+                                       $val = $wgLang->commaList( $flashMsgs );
+                                       break;
+
+                               case 'FocalPlaneResolutionUnit':
+                                       switch( $val ) {
+                                       case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'SensingMethod':
+                                       switch( $val ) {
+                                       case 1: case 2: case 3: case 4: case 5: case 7: case 8:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'FileSource':
+                                       switch( $val ) {
+                                       case 3:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'SceneType':
+                                       switch( $val ) {
+                                       case 1:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'CustomRendered':
+                                       switch( $val ) {
+                                       case 0: case 1:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'ExposureMode':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'WhiteBalance':
+                                       switch( $val ) {
+                                       case 0: case 1:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'SceneCaptureType':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GainControl':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3: case 4:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'Contrast':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'Saturation':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'Sharpness':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'SubjectDistanceRange':
+                                       switch( $val ) {
+                                       case 0: case 1: case 2: case 3:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               //The GPS...Ref values are kept for compatibility, probably won't be reached.
+                               case 'GPSLatitudeRef':
+                               case 'GPSDestLatitudeRef':
+                                       switch( $val ) {
+                                       case 'N': case 'S':
+                                               $val = self::msg( 'GPSLatitude', $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSLongitudeRef':
+                               case 'GPSDestLongitudeRef':
+                                       switch( $val ) {
+                                       case 'E': case 'W':
+                                               $val = self::msg( 'GPSLongitude', $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSAltitude':
+                                       if ( $val < 0 ) {
+                                               $val = self::msg( 'GPSAltitude', 'below-sealevel', self::formatNum( -$val, 3 ) );
+                                       } else {
+                                               $val = self::msg( 'GPSAltitude', 'above-sealevel', self::formatNum( $val, 3 ) );
+                                       }
+                                       break;
+
+                               case 'GPSStatus':
+                                       switch( $val ) {
+                                       case 'A': case 'V':
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSMeasureMode':
+                                       switch( $val ) {
+                                       case 2: case 3:
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+
+                               case 'GPSTrackRef':
+                               case 'GPSImgDirectionRef':
+                               case 'GPSDestBearingRef':
+                                       switch( $val ) {
+                                       case 'T': case 'M':
+                                               $val = self::msg( 'GPSDirection', $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSLatitude':
+                               case 'GPSDestLatitude':
+                                       $val = self::formatCoords( $val, 'latitude' );
+                                       break;
+                               case 'GPSLongitude':
+                               case 'GPSDestLongitude':
+                                       $val = self::formatCoords( $val, 'longitude' );
+                                       break;
+
+                               case 'GPSSpeedRef':
+                                       switch( $val ) {
+                                       case 'K': case 'M': case 'N':
+                                               $val = self::msg( 'GPSSpeed', $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSDestDistanceRef':
+                                       switch( $val ) {
+                                       case 'K': case 'M': case 'N':
+                                               $val = self::msg( 'GPSDestDistance', $val );
+                                               break;
+                                       default:
+                                               $val = $val;
+                                               break;
+                                       }
+                                       break;
+
+                               case 'GPSDOP':
+                                       // See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)
+                                       if ( $val <= 2 ) {
+                                               $val = self::msg( $tag, 'excellent', self::formatNum( $val ) );
+                                       } elseif ( $val <= 5 ) {
+                                               $val = self::msg( $tag, 'good', self::formatNum( $val ) );
+                                       } elseif ( $val <= 10 ) {
+                                               $val = self::msg( $tag, 'moderate', self::formatNum( $val ) );
+                                       } elseif ( $val <= 20 ) {
+                                               $val = self::msg( $tag, 'fair', self::formatNum( $val ) );
+                                       } else {
+                                               $val = self::msg( $tag, 'poor', self::formatNum( $val ) );
+                                       }
+                                       break;
+
+       
+
+                               // This is not in the Exif standard, just a special
+                               // case for our purposes which enables wikis to wikify
+                               // the make, model and software name to link to their articles.
+                               case 'Make':
+                               case 'Model':
+                                       $val = self::msg( $tag, '', $val );
+                                       break;
+
+                               case 'Software':
+                                       if ( is_array( $val ) ) {
+                                               //if its a software, version array.
+                                               $val = wfMsg( 'exif-software-version-value', $val[0], $val[1] );
+                                       } else {
+                                               $val = self::msg( $tag, '', $val );
+                                       }
+                                       break;
+
+                               case 'ExposureTime':
+                                       // Show the pretty fraction as well as decimal version
+                                       $val = wfMsg( 'exif-exposuretime-format',
+                                               self::formatFraction( $val ), self::formatNum( $val ) );
+                                       break;
+                               case 'ISOSpeedRatings':
+                                       // If its = 65535 that means its at the
+                                       // limit of the size of Exif::short and
+                                       // is really higher.
+                                       if ( $val == '65535' ) {
+                                               $val = self::msg( $tag, 'overflow' );
+                                       } else {
+                                               $val = self::formatNum( $val );
+                                       }
+                                       break;
+                               case 'FNumber':
+                                       $val = wfMsg( 'exif-fnumber-format',
+                                               self::formatNum( $val ) );
+                                       break;
+
+                               case 'FocalLength': case 'FocalLengthIn35mmFilm':
+                                       $val = wfMsg( 'exif-focallength-format',
+                                               self::formatNum( $val ) );
+                                       break;
+
+                               case 'MaxApertureValue':
+                                       if ( strpos( $val, '/' ) !== false ) {
+                                               // need to expand this earlier to calculate fNumber
+                                               list($n, $d) = explode('/', $val);
+                                               if ( is_numeric( $n ) && is_numeric( $d ) ) {
+                                                       $val = $n / $d;
+                                               }
+                                       }
+                                       if ( is_numeric( $val ) ) {
+                                               $fNumber = pow( 2, $val / 2 );
+                                               if ( $fNumber !== false ) {
+                                                       $val = wfMsg( 'exif-maxaperturevalue-value',
+                                                               self::formatNum( $val ),
+                                                               self::formatNum( $fNumber, 2 )
+                                                       );
+                                               }
+                                       }
+                                       break;
+                                       
+                               case 'iimCategory':
+                                       switch( strtolower( $val ) ) {
+                                               // See pg 29 of IPTC photo
+                                               // metadata standard.
+                                               case 'ace': case 'clj':
+                                               case 'dis': case 'fin':
+                                               case 'edu': case 'evn':
+                                               case 'hth': case 'hum':
+                                               case 'lab': case 'lif':
+                                               case 'pol': case 'rel':
+                                               case 'sci': case 'soi':
+                                               case 'spo': case 'war':
+                                               case 'wea':
+                                                       $val = self::msg(
+                                                               'iimcategory',
+                                                               $val
+                                                       );
+                                       }
+                                       break;
+                               case 'SubjectNewsCode':
+                                       // Essentially like iimCategory.
+                                       // 8 (numeric) digit hierarchical
+                                       // classification. We decode the
+                                       // first 2 digits, which provide
+                                       // a broad category.
+                                       $val = self::convertNewsCode( $val );
+                                       break;
+                               case 'Urgency':
+                                       // 1-8 with 1 being highest, 5 normal
+                                       // 0 is reserved, and 9 is 'user-defined'.
+                                       $urgency = '';
+                                       if ( $val == 0 || $val == 9 ) {
+                                               $urgency = 'other';
+                                       } elseif ( $val < 5 && $val > 1 ) {
+                                               $urgency = 'high';
+                                       } elseif ( $val == 5 ) {
+                                               $urgency = 'normal';
+                                       } elseif ( $val <= 8 && $val > 5) {
+                                               $urgency = 'low';
+                                       }
+
+                                       if ( $urgency !== '' ) {
+                                               $val = self::msg( 'urgency',
+                                                       $urgency, $val
+                                               );
+                                       }
+                                       break;
+
+                               // Things that have a unit of pixels.
+                               case 'OriginalImageHeight':
+                               case 'OriginalImageWidth':
+                               case 'PixelXDimension':
+                               case 'PixelYDimension':
+                               case 'ImageWidth':
+                               case 'ImageLength':
+                                       $val = self::formatNum( $val ) . ' ' . wfMsg( 'unit-pixel' );
+                                       break;
+
+                               // Do not transform fields with pure text.
+                               // For some languages the formatNum()
+                               // conversion results to wrong output like
+                               // foo,bar@example,com or foo٫bar@example٫com.
+                               // Also some 'numeric' things like Scene codes
+                               // are included here as we really don't want
+                               // commas inserted.
+                               case 'ImageDescription':
+                               case 'Artist':
+                               case 'Copyright':
+                               case 'RelatedSoundFile':
+                               case 'ImageUniqueID':
+                               case 'SpectralSensitivity':
+                               case 'GPSSatellites':
+                               case 'GPSVersionID':
+                               case 'GPSMapDatum':
+                               case 'Keywords':
+                               case 'WorldRegionDest':
+                               case 'CountryDest':
+                               case 'CountryCodeDest':
+                               case 'ProvinceOrStateDest':
+                               case 'CityDest':
+                               case 'SublocationDest':
+                               case 'WorldRegionCreated':
+                               case 'CountryCreated':
+                               case 'CountryCodeCreated':
+                               case 'ProvinceOrStateCreated':
+                               case 'CityCreated':
+                               case 'SublocationCreated':
+                               case 'ObjectName':
+                               case 'SpecialInstructions':
+                               case 'Headline':
+                               case 'Credit':
+                               case 'Source':
+                               case 'EditStatus':
+                               case 'FixtureIdentifier':
+                               case 'LocationDest':
+                               case 'LocationDestCode':
+                               case 'Writer':
+                               case 'JPEGFileComment':
+                               case 'iimSupplementalCategory':
+                               case 'OriginalTransmissionRef':
+                               case 'Identifier':
+                               case 'dc-contributor':
+                               case 'dc-coverage':
+                               case 'dc-publisher':
+                               case 'dc-relation':
+                               case 'dc-rights':
+                               case 'dc-source':
+                               case 'dc-type':
+                               case 'Lens':
+                               case 'SerialNumber':
+                               case 'CameraOwnerName':
+                               case 'Label':
+                               case 'Nickname':
+                               case 'RightsCertificate':
+                               case 'CopyrightOwner':
+                               case 'UsageTerms':
+                               case 'WebStatement':
+                               case 'OriginalDocumentID':
+                               case 'LicenseUrl':
+                               case 'MorePermissionsUrl':
+                               case 'AttributionUrl':
+                               case 'PreferredAttributionName':
+                               case 'PNGFileComment':
+                               case 'Disclaimer':
+                               case 'ContentWarning':
+                               case 'GIFFileComment':
+                               case 'SceneCode':
+                               case 'IntellectualGenre':
+                               case 'Event':
+                               case 'OrginisationInImage':
+                               case 'PersonInImage':
+
+                                       $val = htmlspecialchars( $val );
+                                       break;
+
+                               case 'ObjectCycle':
+                                       switch ( $val ) {
+                                       case 'a': case 'p': case 'b':
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       default:
+                                               $val = htmlspecialchars( $val );
+                                               break;
+                                       }
+                                       break;
+                               case 'Copyrighted':
+                                       switch( $val ) {
+                                       case 'True': case 'False':
+                                               $val = self::msg( $tag, $val );
+                                               break;
+                                       }
+                                       break;
+                               case 'Rating':
+                                       if ( $val == '-1' ) {
+                                               $val = self::msg( $tag, 'rejected' );
+                                       } else {
+                                               $val = self::formatNum( $val );
+                                       }
+                                       break;
+
+                               case 'LanguageCode':
+                                       $lang = $wgLang->getLanguageName( strtolower( $val ) );
+                                       if ($lang) {
+                                               $val = htmlspecialchars( $lang );
+                                       } else {
+                                               $val = htmlspecialchars( $val );
+                                       }
+                                       break;
+
+                               default:
+                                       $val = self::formatNum( $val );
+                                       break;
+                               }
+                       }
+                       // End formatting values, start flattening arrays.
+                       $vals = self::flattenArray( $vals, $type );
+
+               }
+               return $tags;
+       }
+
+       /**
+       * A function to collapse multivalued tags into a single value.
+       * This turns an array of (for example) authors into a bulleted list.
+       *
+       * This is public on the basis it might be useful outside of this class.
+       * 
+       * @param $vals Array array of values
+       * @param $type String Type of array (either lang, ul, ol).
+       * lang = language assoc array with keys being the lang code
+       * ul = unordered list, ol = ordered list
+       * type can also come from the '_type' member of $vals.
+       * @param $noHtml Boolean If to avoid returning anything resembling
+       * html. (Ugly hack for backwards compatibility with old mediawiki). 
+       * @return String single value (in wiki-syntax).
+       */
+       public static function flattenArray( $vals, $type = 'ul', $noHtml = false ) {
+               if ( isset( $vals['_type'] ) ) {
+                       $type = $vals['_type'];
+                       unset( $vals['_type'] );
+               }
+
+               if ( !is_array( $vals ) ) {
+                        return $vals; // do nothing if not an array;
+               }
+               elseif ( count( $vals ) === 1 && $type !== 'lang' ) {
+                       return $vals[0];
+               }
+               elseif ( count( $vals ) === 0 ) {
+                       return ""; // paranoia. This should never happen
+                       wfDebug( __METHOD__ . ' metadata array with 0 elements!' );
+               }
+               /* Fixme: This should hide some of the list entries if there are
+               * say more than four. Especially if a field is translated into 20
+               * languages, we don't want to show them all by default
+               */
+               else {
+                       switch( $type ) {
+                       case 'lang':
+                               global $wgContLang;
+                               // Display default, followed by ContLang,
+                               // followed by the rest in no particular
+                               // order.
+
+                               // Todo: hide some items if really long list.
+
+                               $content = '';
+
+                               $cLang = $wgContLang->getCode();
+                               $defaultItem = false;
+                               $defaultLang = false;
+
+                               // If default is set, save it for later,
+                               // as we don't know if it's equal to
+                               // one of the lang codes. (In xmp
+                               // you specify the language for a 
+                               // default property by having both
+                               // a default prop, and one in the language
+                               // that are identical)
+                               if ( isset( $vals['x-default'] ) ) {
+                                       $defaultItem = $vals['x-default'];
+                                       unset( $vals['x-default'] );
+                               }
+                               // Do contentLanguage.
+                               if ( isset( $vals[$cLang] ) ) {
+                                       $isDefault = false;
+                                       if ( $vals[$cLang] === $defaultItem ) {
+                                               $defaultItem = false;
+                                               $isDefault = true;
+                                       }
+                                       $content .= self::langItem(
+                                               $vals[$cLang], $cLang,
+                                                $isDefault, $noHtml );
+
+                                       unset( $vals[$cLang] );
+                               }
+
+                               // Now do the rest.
+                               foreach ( $vals as $lang => $item ) {
+                                       if ( $item === $defaultItem ) {
+                                               $defaultLang = $lang;
+                                               continue;
+                                       }
+                                       $content .= self::langItem( $item,
+                                               $lang, false, $noHtml );
+                               }
+                               if ( $defaultItem !== false ) {
+                                       $content = self::langItem( $defaultItem,
+                                               $defaultLang, true, $noHtml )
+                                                . $content;
+                               }
+                               if ( $noHtml ) {
+                                       return $content;
+                               }
+                               return '<ul class="metadata-langlist">' .
+                                       $content .
+                                       '</ul>';
+                       case 'ol':
+                               if ( $noHtml ) {
+                                       return "\n#" . implode( "\n#", $vals );
+                               }
+                               return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>';
+                       case 'ul':
+                       default:
+                               if ( $noHtml ) {
+                                       return "\n*" . implode( "\n*", $vals );
+                               }
+                               return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>';
+                       }
+               }
+       }
+       /** Helper function for creating lists of translations.
+        *
+        * @param $value String value (this is not escaped)
+        * @param $lang String lang code of item or false
+        * @param $default Boolean if it is default value.
+        * @param $noHtml Boolean If to avoid html (for back-compat)
+        * @return language item (Note: despite how this looks,
+        *      this is treated as wikitext not html).
+        */
+       private static function langItem( $value, $lang, $default = false, $noHtml = false ) {
+               global $wgContLang;
+               if ( $lang === false && $default === false) {
+                       throw new MWException('$lang and $default cannot both '
+                               . 'be false.');
+               }
+
+               if ( $noHtml ) {
+                       $wrappedValue = $value;
+               } else {
+                       $wrappedValue = '<span class="mw-metadata-lang-value">'
+                               . $value . '</span>';
+               }
+
+               if ( $lang === false ) {
+                       if ( $noHtml ) {
+                               return wfMsg( 'metadata-langitem-default',
+                                       $wrappedValue ) . "\n\n";
+                       } /* else */
+                       return '<li class="mw-metadata-lang-default">'
+                               . wfMsg( 'metadata-langitem-default',
+                                       $wrappedValue )
+                               . "</li>\n";
+               }
+
+               $lowLang = strtolower( $lang );
+               $langName = $wgContLang->getLanguageName( $lowLang );
+               if ( $langName === '' ) {
+                       //try just the base language name. (aka en-US -> en ).
+                       list( $langPrefix ) = explode( '-', $lowLang, 2 );
+                       $langName = $wgContLang->getLanguageName( $langPrefix );
+                       if ( $langName === '' ) {
+                               // give up.
+                               $langName = $lang;
+                       }
+               }
+               // else we have a language specified
+
+               if ( $noHtml ) {
+                       return '*' . wfMsg( 'metadata-langitem',
+                               $wrappedValue, $langName, $lang );
+               } /* else: */
+
+               $item = '<li class="mw-metadata-lang-code-'
+                       . $lang;
+               if ( $default ) {
+                       $item .= ' mw-metadata-lang-default';
+               }
+               $item .= '" lang="' . $lang . '">';
+               $item .= wfMsg( 'metadata-langitem',
+                       $wrappedValue, $langName, $lang );
+               $item .= "</li>\n";
+               return $item;
+       }
+       /**
+        * Convenience function for getFormattedData()
+        *
+        * @private
+        *
+        * @param $tag String: the tag name to pass on
+        * @param $val String: the value of the tag
+        * @param $arg String: an argument to pass ($1)
+        * @param $arg2 String: a 2nd argument to pass ($2)
+        * @return string A wfMsg of "exif-$tag-$val" in lower case
+        */
+       static function msg( $tag, $val, $arg = null, $arg2 = null ) {
+               global $wgContLang;
+
+               if ($val === '')
+                       $val = 'value';
+               return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 );
+       }
+
+       /**
+        * Format a number, convert numbers from fractions into floating point
+        * numbers, joins arrays of numbers with commas.
+        *
+        * @private
+        *
+        * @param $num Mixed: the value to format
+        * @param $round digits to round to or false.
+        * @return mixed A floating point number or whatever we were fed
+        */
+       static function formatNum( $num, $round = false ) {
+               global $wgLang;
+               $m = array();
+               if( is_array($num) ) {
+                       $out = array();
+                       foreach( $num as $number ) {
+                               $out[] = self::formatNum($number);
+                       }
+                       return $wgLang->commaList( $out );
+               }
+               if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
+                       if ( $m[2] != 0 ) {
+                               $newNum = $m[1] / $m[2];
+                               if ( $round !== false ) {
+                                       $newNum = round( $newNum, $round );
+                               }
+                       } else {
+                               $newNum = $num;
+                       }
+
+                       return $wgLang->formatNum( $newNum );
+               } else {
+                       if ( is_numeric( $num ) && $round !== false ) {
+                               $num = round( $num, $round );
+                       }
+                       return $wgLang->formatNum( $num );
+               }
+       }
+
+       /**
+        * Format a rational number, reducing fractions
+        *
+        * @private
+        *
+        * @param $num Mixed: the value to format
+        * @return mixed A floating point number or whatever we were fed
+        */
+       static function formatFraction( $num ) {
+               $m = array();
+               if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) {
+                       $numerator = intval( $m[1] );
+                       $denominator = intval( $m[2] );
+                       $gcd = self::gcd( abs( $numerator ), $denominator );
+                       if( $gcd != 0 ) {
+                               // 0 shouldn't happen! ;)
+                               return self::formatNum( $numerator / $gcd ) . '/' . self::formatNum( $denominator / $gcd );
+                       }
+               }
+               return self::formatNum( $num );
+       }
+
+       /**
+        * Calculate the greatest common divisor of two integers.
+        *
+        * @param $a Integer: Numerator
+        * @param $b Integer: Denominator
+        * @return int
+        * @private
+        */
+       static function gcd( $a, $b ) {
+               /*
+                       // http://en.wikipedia.org/wiki/Euclidean_algorithm
+                       // Recursive form would be:
+                       if( $b == 0 )
+                               return $a;
+                       else
+                               return gcd( $b, $a % $b );
+               */
+               while( $b != 0 ) {
+                       $remainder = $a % $b;
+
+                       // tail recursion...
+                       $a = $b;
+                       $b = $remainder;
+               }
+               return $a;
+       }
+
+       /** Fetch the human readable version of a news code.
+        * A news code is an 8 digit code. The first two 
+        * digits are a general classification, so we just
+        * translate that.
+        *
+        * Note, leading 0's are significant, so this is
+        * a string, not an int.
+        *
+        * @param $val String: The 8 digit news code.
+        * @return The human readable form
+        */
+       static private function convertNewsCode( $val ) {
+               if ( !preg_match( '/^\d{8}$/D', $val ) ) {
+                       // Not a valid news code.
+                       return $val;
+               }
+               $cat = '';
+               switch( substr( $val , 0, 2 ) ) {
+                       case '01':
+                               $cat = 'ace';
+                               break;
+                       case '02':
+                               $cat = 'clj';
+                               break;
+                       case '03':
+                               $cat = 'dis';
+                               break;
+                       case '04':
+                               $cat = 'fin';
+                               break;
+                       case '05':
+                               $cat = 'edu';
+                               break;
+                       case '06':
+                               $cat = 'evn';
+                               break;
+                       case '07':
+                               $cat = 'hth';
+                               break;
+                       case '08':
+                               $cat = 'hum';
+                               break;
+                       case '09':
+                               $cat = 'lab';
+                               break;
+                       case '10':
+                               $cat = 'lif';
+                               break;
+                       case '11':
+                               $cat = 'pol';
+                               break;
+                       case '12':
+                               $cat = 'rel';
+                               break;
+                       case '13':
+                               $cat = 'sci';
+                               break;
+                       case '14':
+                               $cat = 'soi';
+                               break;
+                       case '15':
+                               $cat = 'spo';
+                               break;
+                       case '16':
+                               $cat = 'war';
+                               break;
+                       case '17':
+                               $cat = 'wea';
+                               break;
+               }
+               if ( $cat !== '' ) {
+                       $catMsg = self::msg( 'iimcategory', $cat );
+                       $val = self::msg( 'subjectnewscode', '', $val, $catMsg );
+               }
+               return $val;
+       }
+       /**
+        * Format a coordinate value, convert numbers from floating point
+        * into degree minute second representation.
+        *
+        * @private
+        *
+        * @param $coords Array: degrees, minutes and seconds
+        * @param $type String: latitude or longitude (for if its a NWS or E)
+        * @return mixed A floating point number or whatever we were fed
+        */
+       static function formatCoords( $coord, $type ) {
+               $ref = '';
+               if ( $coord < 0 ) {
+                       $nCoord = -$coord;
+                       if ( $type === 'latitude' ) {
+                               $ref = 'S';
+                       }
+                       elseif ( $type === 'longitude' ) {
+                               $ref = 'W';
+                       }
+               }
+               else {
+                       $nCoord = $coord;
+                       if ( $type === 'latitude' ) {
+                               $ref = 'N';
+                       }
+                       elseif ( $type === 'longitude' ) {
+                               $ref = 'E';
+                       }
+               }
+
+               $deg = floor( $nCoord );
+               $min = floor( ( $nCoord - $deg ) * 60.0 );
+               $sec = round( ( ( $nCoord - $deg ) - $min / 60 ) * 3600, 2 );
+
+               $deg = self::formatNum( $deg );
+               $min = self::formatNum( $min );
+               $sec = self::formatNum( $sec );
+
+               return wfMsg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord );
+       }
+       /**
+        * Format the contact info field into a single value.
+        *
+        * @param $vals Array array with fields of the ContactInfo
+        *    struct defined in the IPTC4XMP spec. Or potentially
+        *    an array with one element that is a free form text
+        *    value from the older iptc iim 1:118 prop.
+        *
+        * This function might be called from
+        * JpegHandler::convertMetadataVersion which is why it is
+        * public.
+        *
+        * @return String of html-ish looking wikitext
+        */
+       public function collapseContactInfo( $vals ) {
+               if( ! ( isset( $vals['CiAdrExtadr'] )
+                       || isset( $vals['CiAdrCity'] )
+                       || isset( $vals['CiAdrCtry'] )
+                       || isset( $vals['CiEmailWork'] )
+                       || isset( $vals['CiTelWork'] )
+                       || isset( $vals['CiAdrPcode'] )
+                       || isset( $vals['CiAdrRegion'] )
+                       || isset( $vals['CiUrlWork'] )
+               ) ) {
+                       // We don't have any sub-properties
+                       // This could happen if its using old
+                       // iptc that just had this as a free-form
+                       // text value.
+                       // Note: We run this through htmlspecialchars
+                       // partially to be consistent, and partially
+                       // because people often insert >, etc into
+                       // the metadata which should not be interpreted
+                       // but we still want to auto-link urls.
+                       foreach( $vals as &$val ) {
+                               $val = htmlspecialchars( $val );
+                       }
+                       return self::flattenArray( $vals );
+               } else {
+                       // We have a real ContactInfo field.
+                       // Its unclear if all these fields have to be
+                       // set, so assume they do not.
+                       $url = $tel = $street = $city = $country = '';
+                       $email = $postal = $region = '';
+
+                       // Also note, some of the class names this uses
+                       // are similar to those used by hCard. This is
+                       // mostly because they're sensible names. This
+                       // does not (and does not attempt to) output
+                       // stuff in the hCard microformat. However it
+                       // might output in the adr microformat.
+
+                       if ( isset( $vals['CiAdrExtadr'] ) ) {
+                               // Todo: This can potentially be multi-line.
+                               // Need to check how that works in XMP.
+                               $street = '<span class="extended-address">'
+                                       . htmlspecialchars( 
+                                               $vals['CiAdrExtadr'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiAdrCity'] ) ) {
+                               $city = '<span class="locality">'
+                                       . htmlspecialchars( $vals['CiAdrCity'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiAdrCtry'] ) ) {
+                               $country = '<span class="country-name">'
+                                       . htmlspecialchars( $vals['CiAdrCtry'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiEmailWork'] ) ) {
+                               $emails = array();
+                               // Have to split multiple emails at commas/new lines.
+                               $splitEmails = explode( "\n", $vals['CiEmailWork'] );
+                               foreach ( $splitEmails as $e1 ) {
+                                       // Also split on comma
+                                       foreach ( explode( ',', $e1 ) as $e2 ) {
+                                               $finalEmail = trim( $e2 );
+                                               if ( $finalEmail == ',' || $finalEmail == '' ) {
+                                                       continue;
+                                               }
+                                               if ( strpos( $finalEmail, '<' ) !== false ) {
+                                                       // Don't do fancy formatting to
+                                                       // "My name" <foo@bar.com> style stuff
+                                                       $emails[] = $finalEmail;
+                                               } else {
+                                                       $emails[] = '[mailto:'
+                                                       . $finalEmail
+                                                       . ' <span class="email">'
+                                                       . $finalEmail
+                                                       . '</span>]';
+                                               }
+                                       }
+                               }
+                               $email = implode( ', ', $emails );
+                       }
+                       if ( isset( $vals['CiTelWork'] ) ) {
+                               $tel = '<span class="tel">'
+                                       . htmlspecialchars( $vals['CiTelWork'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiAdrPcode'] ) ) {
+                               $postal = '<span class="postal-code">'
+                                       . htmlspecialchars( 
+                                               $vals['CiAdrPcode'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiAdrRegion'] ) ) {
+                               // Note this is province/state.
+                               $region = '<span class="region">'
+                                       . htmlspecialchars(
+                                               $vals['CiAdrRegion'] )
+                                       . '</span>';
+                       }
+                       if ( isset( $vals['CiUrlWork'] ) ) {
+                               $url = '<span class="url">'
+                                       . htmlspecialchars( $vals['CiUrlWork'] )
+                                       . '</span>';
+                       }
+                       return wfMsg( 'exif-contact-value', $email, $url,
+                               $street, $city, $region, $postal, $country,
+                               $tel );
+               }
+       }
+}
+
+/** For compatability with old FormatExif class
+ * which some extensions use.
+ *
+ *@deprecated
+ *
+**/
+class FormatExif {
+       var $meta;
+       function FormatExif ( $meta ) {
+               wfDeprecated(__METHOD__);
+               $this->meta = $meta;
+       }
+       function getFormattedData ( ) {
+               return FormatMetadata::getFormattedData( $this->meta );
+       }
+
+}
index 0d7529a..cdbde35 100644 (file)
  */
 class GIFHandler extends BitmapHandler {
 
+       const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata.
+       
        function getMetadata( $image, $filename ) {
-               if ( !isset( $image->parsedGIFMetadata ) ) {
-                       try {
-                               $image->parsedGIFMetadata = GIFMetadataExtractor::getMetadata( $filename );
-                       } catch( Exception $e ) {
-                               // Broken file?
-                               wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-                               return '0';
-                       }
+               try {
+                       $parsedGIFMetadata = BitmapMetadataHandler::GIF( $filename );
+               } catch( Exception $e ) {
+                       // Broken file?
+                       wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+                       return self::BROKEN_FILE;
                }
 
-               return serialize( $image->parsedGIFMetadata );
+               return serialize($parsedGIFMetadata);
        }
 
        function formatMetadata( $image ) {
-               return false;
+               $meta = $image->getMetadata();
+
+               if ( !$meta ) {
+                       return false;
+               }
+               $meta = unserialize( $meta );
+                if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) {
+                       return false;
+               }
+
+               if ( isset( $meta['metadata']['_MW_GIF_VERSION'] ) ) {
+                       unset( $meta['metadata']['_MW_GIF_VERSION'] );
+               }
+               return $this->formatMetadataHelper( $meta['metadata'] );
        }
 
        /**
@@ -67,10 +80,26 @@ class GIFHandler extends BitmapHandler {
        }
 
        function isMetadataValid( $image, $metadata ) {
+               if ( $metadata === self::BROKEN_FILE ) {
+                       // Do not repetitivly regenerate metadata on broken file.
+                       return self::METADATA_GOOD;
+               }
+
                wfSuppressWarnings();
                $data = unserialize( $metadata );
                wfRestoreWarnings();
-               return (boolean) $data;
+
+               if ( !$data || !is_array( $data ) ) {
+                       wfDebug(__METHOD__ . ' invalid GIF metadata' );
+                       return self::METADATA_BAD;
+               }
+
+               if ( !isset( $data['metadata']['_MW_GIF_VERSION'] )
+                       || $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION ) {
+                       wfDebug(__METHOD__ . ' old but compatible GIF metadata' );
+                       return self::METADATA_COMPATIBLE;
+               }
+               return self::METADATA_GOOD;
        }
 
        /**
index 8d0e87e..71003b7 100644 (file)
@@ -21,6 +21,13 @@ class GIFMetadataExtractor {
        static $gif_extension_sep;
        static $gif_term;
 
+       const VERSION = 1;
+
+       // Each sub-block is less than or equal to 255 bytes.
+       // Most of the time its 255 bytes, except for in XMP
+       // blocks, where it's usually between 32-127 bytes each.
+       const MAX_SUBBLOCKS = 262144; // 5mb divided by 20.
+
        static function getMetadata( $filename ) {
                self::$gif_frame_sep = pack( "C", ord("," ) );
                self::$gif_extension_sep = pack( "C", ord("!" ) );
@@ -29,7 +36,9 @@ class GIFMetadataExtractor {
                $frameCount = 0;
                $duration = 0.0;
                $isLooped = false;
-
+               $xmp = "";
+               $comment = array();
+               
                if ( !$filename ) {
                        throw new Exception( "No file name specified" );
                } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
@@ -103,37 +112,94 @@ class GIFMetadataExtractor {
                                        if ($term != 0 ) {
                                                throw new Exception( "Malformed Graphics Control Extension block" );
                                        }
+                               } elseif ($extension_code == 0xFE) {
+                                       // Comment block(s).
+                                       $data = '';
+
+                                       $data = self::readBlock( $fh );
+                                       if ( $data === "" ) {
+                                               throw new Exception( 'Read error, zero-length comment block' );
+                                       }
+
+                                       // The standard says this should be ASCII, however its unclear if
+                                       // thats true in practise. Check to see if its valid utf-8, if so
+                                       // assume its that, otherwise assume its iso-8859-1
+                                       $dataCopy = $data;
+                                       // quickIsNFCVerify has the side effect of replacing any invalid characters
+                                       UtfNormal::quickIsNFCVerify( $dataCopy );
+
+                                       if ( $dataCopy !== $data ) {
+                                               wfSuppressWarnings();
+                                               $data = iconv( 'ISO-8859-1', 'UTF-8', $data );
+                                               wfRestoreWarnings();
+                                       }
+
+                                       $commentCount = count( $comment );
+                                       if ( $commentCount === 0
+                                               || $comment[$commentCount-1] !== $data )
+                                       {
+                                               // Some applications repeat the same comment on each
+                                               // frame of an animated GIF image, so if this comment
+                                               // is identical to the last, only extract once.
+                                               $comment[] = $data;
+                                       }
                                } elseif ($extension_code == 0xFF) {
                                        // Application extension (Netscape info about the animated gif)
+                                       // or XMP (or theoretically any other type of extension block)
                                        $blockLength = fread( $fh, 1 );
                                        $blockLength = unpack( 'C', $blockLength );
                                        $blockLength = $blockLength[1];
                                        $data = fread( $fh, $blockLength );
 
-                                       // NETSCAPE2.0 (application name)
-                                       if ($blockLength != 11 || $data != 'NETSCAPE2.0') {
+                                       if ($blockLength != 11 ) {
+                                               wfDebug( __METHOD__ . ' GIF application block with wrong length' );
                                                fseek( $fh, -($blockLength + 1), SEEK_CUR );
                                                self::skipBlock( $fh );
                                                continue;
                                        }
 
-                                       $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
-
-                                       if ($data != "\x03\x01") {
-                                               throw new Exception( "Expected \x03\x01, got $data" );
-                                       }
-
-                                       // Unsigned little-endian integer, loop count or zero for "forever"
-                                       $loopData = fread( $fh, 2 );
-                                       $loopData = unpack( 'v', $loopData );
-                                       $loopCount = $loopData[1];
-
-                                       if ($loopCount != 1) {
-                                               $isLooped = true;
+                                       // NETSCAPE2.0 (application name for animated gif)
+                                       if ( $data == 'NETSCAPE2.0' ) {
+                                       
+                                               $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
+
+                                               if ($data != "\x03\x01") {
+                                                       throw new Exception( "Expected \x03\x01, got $data" );
+                                               }
+                                               
+                                               // Unsigned little-endian integer, loop count or zero for "forever"
+                                               $loopData = fread( $fh, 2 );
+                                               $loopData = unpack( 'v', $loopData );
+                                               $loopCount = $loopData[1];
+                                               
+                                               if ($loopCount != 1) {
+                                                       $isLooped = true;
+                                               }
+                                               
+                                               // Read out terminator byte
+                                               fread( $fh, 1 );
+                                       } elseif ( $data == 'XMP DataXMP' ) {
+                                               // application name for XMP data.
+                                               // see pg 18 of XMP spec part 3.
+
+                                               $xmp = self::readBlock( $fh, true );
+
+                                               if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE"
+                                                       || substr( $xmp, -4 ) !== "\x03\x02\x01\x00" )
+                                               {
+                                                       // this is just a sanity check.
+                                                       throw new Exception( "XMP does not have magic trailer!" );
+                                               }
+
+                                               // strip out trailer.
+                                               $xmp = substr( $xmp, 0, -257 );
+
+                                       } else {
+                                               // unrecognized extension block
+                                               fseek( $fh, -($blockLength + 1), SEEK_CUR );
+                                               self::skipBlock( $fh );
+                                               continue;
                                        }
-
-                                       // Read out terminator byte
-                                       fread( $fh, 1 );
                                } else {
                                        self::skipBlock( $fh );
                                }
@@ -149,7 +215,9 @@ class GIFMetadataExtractor {
                return array(
                        'frameCount' => $frameCount,
                        'looped' => $isLooped,
-                       'duration' => $duration
+                       'duration' => $duration,
+                       'xmp' => $xmp,
+                       'comment' => $comment,
                );
        }
 
@@ -183,4 +251,40 @@ class GIFMetadataExtractor {
                        fread( $fh, $block_len );
                }
        }
+       /**
+        * Read a block. In the GIF format, a block is made up of
+        * several sub-blocks. Each sub block starts with one byte
+        * saying how long the sub-block is, followed by the sub-block.
+        * The entire block is terminated by a sub-block of length
+        * 0.
+        * @param $fh FileHandle
+        * @param $includeLengths Boolean Include the length bytes of the
+        *  sub-blocks in the returned value. Normally this is false,
+        *  except XMP is weird and does a hack where you need to keep
+        *  these length bytes.
+        * @return The data.
+        */
+       static function readBlock( $fh, $includeLengths = false ) {
+               $data = '';
+               $subLength = fread( $fh, 1 );
+               $blocks = 0;
+
+               while( $subLength !== "\0" ) {
+                       $blocks++;
+                       if ( $blocks > self::MAX_SUBBLOCKS ) {
+                               throw new Exception( "MAX_SUBBLOCKS exceeded (over $blocks sub-blocks)" );
+                       }
+                       if ( feof( $fh ) ) {
+                               throw new Exception( "Read error: Unexpected EOF." );
+                       }
+                       if ( $includeLengths ) {
+                               $data .= $subLength;
+                       }
+
+                       $data .= fread( $fh, ord( $subLength ) );
+                       $subLength = fread( $fh, 1 );
+               }
+               return $data;
+       }
+
 }
index 2e092f2..0db9ae8 100644 (file)
@@ -13,7 +13,9 @@
  */
 abstract class MediaHandler {
        const TRANSFORM_LATER = 1;
-
+       const METADATA_GOOD = true;
+       const METADATA_BAD = false;
+       const METADATA_COMPATIBLE = 2; // for old but backwards compatible.
        /**
         * Instance cache
         */
@@ -90,6 +92,47 @@ abstract class MediaHandler {
         */
        function getMetadata( $image, $path ) { return ''; }
 
+       /**
+       * Get metadata version.
+       *
+       * This is not used for validating metadata, this is used for the api when returning
+       * metadata, since api content formats should stay the same over time, and so things
+       * using ForiegnApiRepo can keep backwards compatibility
+       *
+       * All core media handlers share a common version number, and extensions can
+       * use the GetMetadataVersion hook to append to the array (they should append a unique
+       * string so not to get confusing). If there was a media handler named 'foo' with metadata
+       * version 3 it might add to the end of the array the element 'foo=3'. if the core metadata
+       * version is 2, the end version string would look like '2;foo=3'.
+       *
+       * @return string version string
+       */
+       static function getMetadataVersion () {
+               $version = Array( '2' ); // core metadata version
+               wfRunHooks('GetMetadataVersion', Array(&$version));
+               return implode( ';', $version);
+        }
+
+       /**
+       * Convert metadata version.
+       *
+       * By default just returns $metadata, but can be used to allow
+       * media handlers to convert between metadata versions.
+       *
+       * @param $metadata Mixed String or Array metadata array (serialized if string)
+       * @param $version Integer target version
+       * @return Array serialized metadata in specified version, or $metadata on fail.
+       */
+       function convertMetadataVersion( $metadata, $version = 1 ) {
+               if ( !is_array( $metadata ) ) {
+                       //unserialize to keep return parameter consistent.
+                       wfSuppressWarnings();
+                       return unserialize( $metadata );
+                       wfRestoreWarnings();
+               }
+               return $metadata;
+       }
+
        /**
         * Get a string describing the type of metadata, for display purposes.
         */
@@ -97,9 +140,15 @@ abstract class MediaHandler {
 
        /**
         * Check if the metadata string is valid for this handler.
-        * If it returns false, Image will reload the metadata from the file and update the database
-        */
-       function isMetadataValid( $image, $metadata ) { return true; }
+        * If it returns MediaHandler::METADATA_BAD (or false), Image
+        * will reload the metadata from the file and update the database.
+        * MediaHandler::METADATA_GOOD for if the metadata is a-ok,
+        * MediaHanlder::METADATA_COMPATIBLE if metadata is old but backwards
+        * compatible (which may or may not trigger a metadata reload).
+        */
+       function isMetadataValid( $image, $metadata ) { 
+               return self::METADATA_GOOD;
+       }
 
 
        /**
@@ -239,18 +288,98 @@ abstract class MediaHandler {
                return false;
        }
 
+       /** sorts the visible/invisible field.
+        * Split off from ImageHandler::formatMetadata, as used by more than
+        * one type of handler.
+        *
+        * This is used by the media handlers that use the FormatMetadata class
+        *
+        * @param $metadataArray Array metadata array
+        * @return array for use displaying metadata.
+        */
+       function formatMetadataHelper( $metadataArray ) {
+                $result = array(
+                       'visible' => array(),
+                       'collapsed' => array()
+               );
+
+               $formatted = FormatMetadata::getFormattedData( $metadataArray );
+               // Sort fields into visible and collapsed
+               $visibleFields = $this->visibleMetadataFields();
+               foreach ( $formatted as $name => $value ) {
+                       $tag = strtolower( $name );
+                       self::addMeta( $result,
+                               in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed',
+                               'exif',
+                               $tag,
+                               $value
+                       );
+               }
+               return $result;
+       }
+
        /**
-        * @todo Fixme: document this!
-        * 'value' thingy goes into a wikitext table; it used to be escaped but
-        * that was incompatible with previous practice of customized display
+        * Get a list of metadata items which should be displayed when
+        * the metadata table is collapsed.
+        *
+        * @return array of strings
+        * @access private
+        */
+       function visibleMetadataFields() {
+               $fields = array();
+               $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) );
+               foreach( $lines as $line ) {
+                       $matches = array();
+                       if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) {
+                               $fields[] = $matches[1];
+                       }
+               }
+               $fields = array_map( 'strtolower', $fields );
+               return $fields;
+       }
+
+
+       /**
+        * This is used to generate an array element for each metadata value
+        * That array is then used to generate the table of metadata values
+        * on the image page 
+        *
+        * @param &$array Array An array containing elements for each type of visibility
+        * and each of those elements being an array of metadata items. This function adds
+        * a value to that array.
+        * @param $visibility String ('visible' or 'collapsed') if this value is hidden
+        * by default.
+        * @param $type String type of metadata tag (currently always 'exif')
+        * @param $id String the name of the metadata tag (like 'artist' for example).
+        * its name in the table displayed is the message "$type-$id" (Ex exif-artist ).
+        * @param $value String thingy goes into a wikitext table; it used to be escaped but
+        * that was incompatible with previous practise of customized display
         * with wikitext formatting via messages such as 'exif-model-value'.
         * So the escaping is taken back out, but generally this seems a confusing
         * interface.
+        * @param $param String value to pass to the message for the name of the field
+        * as $1. Currently this parameter doesn't seem to ever be used.
+        *
+        * @return Array $array but with the new metadata field added.
+        *
+        * Note, everything here is passed through the parser later on (!)
         */
        protected static function addMeta( &$array, $visibility, $type, $id, $value, $param = false ) {
+               $msgName = "$type-$id";
+               if ( wfEmptyMsg( $msgName ) ) {
+                       // This is for future compatibility when using instant commons.
+                       // So as to not display as ugly a name if a new metadata 
+                       // property is defined that we don't know about
+                       // (not a major issue since such a property would be collapsed
+                       // by default).
+                       wfDebug( __METHOD__ . ' Unknown metadata name: ' . $id . "\n" );
+                       $name = wfEscapeWikiText( $id );
+               } else {
+                       $name = wfMsg( $msgName, $param );
+               }
                $array[$visibility][] = array(
                        'id' => "$type-$id",
-                       'name' => wfMsg( "$type-$id", $param ),
+                       'name' => $name,
                        'value' => $value
                );
        }
diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php
new file mode 100644 (file)
index 0000000..73a12b2
--- /dev/null
@@ -0,0 +1,570 @@
+<?php
+/**
+*Class for some IPTC functions.
+
+*/
+class IPTC {
+
+        /**
+        * This takes the results of iptcparse() and puts it into a
+        * form that can be handled by mediawiki. Generally called from
+        * BitmapMetadataHandler::doApp13.
+        *
+        * @see http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
+        *
+        * @param String $data app13 block from jpeg containing iptc/iim data
+        * @return Array iptc metadata array
+        */
+        static function parse( $rawData ) {
+                $parsed = iptcparse( $rawData );
+                $data = Array();
+                if (!is_array($parsed)) {
+                        return $data;
+                }
+
+                $c = '';
+               //charset info contained in tag 1:90.
+               if (isset($parsed['1#090']) && isset($parsed['1#090'][0])) {
+                       $c = self::getCharset($parsed['1#090'][0]);
+                       if ($c === false) {
+                               //Unknown charset. refuse to parse.
+                               //note: There is a different between
+                               //unknown and no charset specified.
+                               return array();
+                       }
+                       unset( $parsed['1#090'] );
+               }
+
+               foreach ( $parsed as $tag => $val ) {
+                       if ( isset( $val[0] ) && trim($val[0]) == '' ) {
+                               wfDebugLog('iptc', "IPTC tag $tag had only whitespace as its value.");
+                               continue;
+                       }
+                       switch( $tag ) {
+                               case '2#120': /*IPTC caption. mapped with exif ImageDescription*/
+                                       $data['ImageDescription'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#116': /* copyright. Mapped with exif copyright */
+                                       $data['Copyright'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#080': /* byline. Mapped with exif Artist */
+                                       /* merge with byline title (2:85)
+                                        * like how exif does it with
+                                        * Title, person. Not sure if this is best
+                                        * approach since we no longer have the two fields
+                                        * separate. each byline title entry corresponds to a
+                                        * specific byline.                          */
+
+                                       $bylines = self::convIPTC( $val, $c );
+                                       if ( isset( $parsed['2#085'] ) ) {
+                                               $titles = self::convIPTC( $parsed['2#085'], $c );
+                                       } else {
+                                               $titles = array();
+                                       }
+
+                                       for ( $i = 0; $i < count( $titles ); $i++ ) {
+                                               if ( isset( $bylines[$i] ) ) {
+                                                       // theoretically this should always be set
+                                                       // but doesn't hurt to be careful.
+                                                       $bylines[$i] = $titles[$i] . ', ' . $bylines[$i];
+                                               }
+                                       }
+                                       $data['Artist'] = $bylines;
+                                       break;
+                               case '2#025': /* keywords */
+                                       $data['Keywords'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#101': /* Country (shown)*/
+                                       $data['CountryDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#095': /* state/province (shown) */
+                                       $data['ProvinceOrStateDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#090': /* city (Shown) */
+                                       $data['CityDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#092': /* sublocation (shown) */
+                                       $data['SublocationDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#005': /* object name/title */
+                                       $data['ObjectName'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#040': /* special instructions */
+                                       $data['SpecialInstructions'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#105': /* headline*/
+                                       $data['Headline'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#110': /* credit */
+                                       /*"Identifies the provider of the objectdata,
+                                        * not necessarily the owner/creator". */
+                                       $data['Credit'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#115': /* source */
+                                       /* "Identifies the original owner of the intellectual content of the
+                                        *objectdata. This could be an agency, a member of an agency or 
+                                        *an individual." */
+                                       $data['Source'] = self::convIPTC( $val, $c );
+                                       break;
+
+                               case '2#007': /* edit status (lead, correction, etc) */
+                                       $data['EditStatus'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#015': /* category. deprecated. max 3 letters in theory, often more */
+                                       $data['iimCategory'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#020': /* category. deprecated. */
+                                       $data['iimSupplementalCategory'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#010': /*urgency (1-8. 1 most, 5 normal, 8 low priority)*/
+                                       $data['Urgency'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#022':
+                                       /* "Identifies objectdata that recurs often and predictably...
+                                        * Example: Euroweather" */
+                                       $data['FixtureIdentifier'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#026':
+                                       /* Content location code (iso 3166 + some custom things)
+                                        * ex: TUR (for turkey), XUN (for UN), XSP (outer space)
+                                        * See wikipedia article on iso 3166 and appendix D of iim std. */
+                                       $data['LocationDestCode'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#027':
+                                       /* Content location name. Full printable name
+                                        * of location of photo. */
+                                       $data['LocationDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#065':
+                                       /* Originating Program.
+                                        * Combine with Program version (2:70) if present.
+                                        */
+                                       $software = self::convIPTC( $val, $c );
+
+                                       if ( count( $software ) !== 1 ) {
+                                               //according to iim standard this cannot have multiple values
+                                               //so if there is more than one, something weird is happening,
+                                               //and we skip it.
+                                               wfDebugLog( 'iptc', 'IPTC: Wrong count on 2:65 Software field' );
+                                               break;
+                                       }
+
+                                       if ( isset( $parsed['2#070'] ) ) {
+                                               //if a version is set for the software.
+                                               $softwareVersion = self::convIPTC( $parsed['2#070'], $c );
+                                               unset($parsed['2#070']);
+                                               $data['Software'] = array( array( $software[0], $softwareVersion[0] ) );
+                                       } else {
+                                               $data['Software'] = $software;
+                                       }
+
+
+                                       break;
+                               case '2#075':
+                                       /* Object cycle.
+                                        * a for morning (am), p for evening, b for both */
+                                       $data['ObjectCycle'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#100':
+                                       /* Country/Primary location code.
+                                        * "Indicates the code of the country/primary location where the
+                                        * intellectual property of the objectdata was created"
+                                        * unclear how this differs from 2#026
+                                        */
+                                       $data['CountryCodeDest'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#103':
+                                       /* original transmission ref.
+                                        * "A code representing the location of original transmission ac-
+                                        * cording to practises of the provider."
+                                       */
+                                       $data['OriginalTransmissionRef'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#118': /*contact*/
+                                       $data['Contact'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#122':
+                                       /* Writer/Editor
+                                        * "Identification of the name of the person involved in the writing,
+                                        * editing or correcting the objectdata or caption/abstract."
+                                        */
+                                       $data['Writer'] = self::convIPTC( $val, $c );
+                                       break;
+                               case '2#135': /* lang code */
+                                       $data['LanguageCode'] = self::convIPTC( $val, $c );
+                                       break;
+
+                               // Start date stuff.
+                               // It doesn't accept incomplete dates even though they are valid
+                               // according to spec.
+                               // Should potentially store timezone as well.
+                               case '2#055':
+                                       //Date created (not date digitized).
+                                       //Maps to exif DateTimeOriginal
+                                       if ( isset( $parsed['2#060'] ) ) {
+                                               $time = $parsed['2#060'];
+                                       } else {
+                                               $time = Array();
+                                       }
+                                       $timestamp =  self::timeHelper( $val, $time, $c );
+                                       if ($timestamp) {
+                                               $data['DateTimeOriginal'] = $timestamp;
+                                       }
+                                       break;
+
+                               case '2#062':
+                                       //Date converted to digital representation.
+                                       //Maps to exif DateTimeDigitized
+                                       if ( isset( $parsed['2#063'] ) ) {
+                                               $time = $parsed['2#063'];
+                                       } else {
+                                               $time = Array();
+                                       }
+                                       $timestamp =  self::timeHelper( $val, $time, $c );
+                                       if ($timestamp) {
+                                               $data['DateTimeDigitized'] = $timestamp;
+                                       }
+                                       break;
+
+                               case '2#030':
+                                       //Date released.
+                                       if ( isset( $parsed['2#035'] ) ) {
+                                               $time = $parsed['2#035'];
+                                       } else {
+                                               $time = Array();
+                                       }
+                                       $timestamp =  self::timeHelper( $val, $time, $c );
+                                       if ($timestamp) {
+                                               $data['DateTimeReleased'] = $timestamp;
+                                       }
+                                       break;
+
+                               case '2#037':
+                                       //Date expires.
+                                       if ( isset( $parsed['2#038'] ) ) {
+                                               $time = $parsed['2#038'];
+                                       } else {
+                                               $time = Array();
+                                       }
+                                       $timestamp =  self::timeHelper( $val, $time, $c );
+                                       if ($timestamp) {
+                                               $data['DateTimeExpires'] = $timestamp;
+                                       }
+                                       break;
+
+                               case '2#000': /* iim version */
+                                       // unlike other tags, this is a 2-byte binary number.
+                                       //technically this is required if there is iptc data
+                                       //but in practise it isn't always there.
+                                       if ( strlen( $val[0] ) == 2 ) {
+                                               //if is just to be paranoid.
+                                               $versionValue = ord( substr( $val[0], 0, 1 ) ) * 256;
+                                               $versionValue += ord( substr( $val[0], 1, 1 ) );
+                                               $data['iimVersion'] = $versionValue;
+                                       }
+                                       break;
+
+                               case '2#004':
+                                       // IntellectualGenere.
+                                       // first 4 characters are an id code
+                                       // That we're not really interested in.
+
+                                       // This prop is weird, since it's
+                                       // allowed to have multiple values
+                                       // in iim 4.1, but not in the XMP
+                                       // stuff. We're going to just
+                                       // extract the first value.
+                                       $con = self::ConvIPTC( $val, $c );
+                                       if ( strlen( $con[0] ) < 5 ) {
+                                               wfDebugLog( 'iptc', 'IPTC: '
+                                                       . '2:04 too short. '
+                                                       . 'Ignoring.' );
+                                                       break;
+                                       }
+                                       $extracted = substr( $con[0], 4 );
+                                       $data['IntellectualGenre'] = $extracted;
+                                       break;
+
+                               case '2#012':
+                                       // Subject News code - this is a compound field
+                                       // at the moment we only extract the subject news
+                                       // code, which is an 8 digit (ascii) number
+                                       // describing the subject matter of the content.
+                                       $codes = self::convIPTC( $val, $c );
+                                       foreach ( $codes as $ic ) {
+                                               $fields = explode(':', $ic, 3 );
+
+                                               if ( count( $fields ) < 2 ||
+                                                       $fields[0] !== 'IPTC' )
+                                               {
+                                                       wfDebugLog( 'IPTC', 'IPTC: '
+                                                               . 'Invalid 2:12 - ' . $ic );
+                                                       break;
+                                               }
+                                               $data['SubjectNewsCode'] = $fields[1];
+                                       }
+                                       break;
+
+                               // purposely does not do 2:125, 2:130, 2:131,
+                               // 2:47, 2:50, 2:45, 2:42, 2:8, 2:3
+                               // 2:200, 2:201, 2:202
+                               // or the audio stuff (2:150 to 2:154)
+
+                               case '2#070':
+                               case '2#060':
+                               case '2#063':
+                               case '2#085':
+                               case '2#038':
+                               case '2#035':
+                                       //ignore. Handled elsewhere.
+                                       break;
+
+                               default:
+                                       wfDebugLog( 'iptc', "Unsupported iptc tag: $tag. Value: " . implode( ',', $val ));
+                                       break;
+                       }
+
+               }
+               return $data;
+       }
+
+       /**
+       * Convert an iptc date and time tags into the exif format
+       *
+       * @todo Potentially this should also capture the timezone offset. 
+       * @param Array $date The date tag
+       * @param Array $time The time tag
+       * @return String Date in exif format.
+       */
+       private static function timeHelper( $date, $time, $c ) {
+               if ( count( $date ) === 1 ) {
+                       //the standard says this should always be 1
+                       //just double checking.
+                       list($date) = self::convIPTC( $date, $c );
+               } else {
+                       return null;
+               }
+
+               if ( count( $time ) === 1 ) {
+                       list($time) = self::convIPTC( $time, $c );
+                       $dateOnly = false;
+               } else {
+                       $time = '000000+0000'; //placeholder
+                       $dateOnly = true;
+               }
+
+               if ( ! ( preg_match('/\d\d\d\d\d\d[-+]\d\d\d\d/', $time) 
+                       && preg_match('/\d\d\d\d\d\d\d\d/', $date)
+                       && substr($date, 0, 4) !== '0000'
+                       && substr($date, 4, 2) !== '00'
+                       && substr($date, 6, 2) !== '00'
+                ) ) {
+                       //something wrong.
+                       // Note, this rejects some valid dates according to iptc spec
+                       // for example: the date 00000400 means the photo was taken in
+                       // April, but the year and day is unknown. We don't process these
+                       // types of incomplete dates atm.
+                       wfDebugLog( 'iptc', "IPTC: invalid time ( $time ) or date ( $date )");
+                       return null;
+               }
+
+               $unixTS = wfTimestamp( TS_UNIX, $date . substr( $time, 0, 6 ));
+               if ( $unixTS === false ) {
+                       wfDebugLog( 'iptc', "IPTC: can't convert date to TS_UNIX: $date $time." );
+                       return null;
+               }
+
+               $tz = ( intval( substr( $time, 7, 2 ) ) *60*60 )
+                       + ( intval( substr( $time, 9, 2 ) ) * 60 );
+
+               if ( substr( $time, 6, 1 ) === '-' ) {
+                       $tz = - $tz;
+               }
+
+               $finalTimestamp = wfTimestamp( TS_EXIF, $unixTS + $tz );
+               if ( $finalTimestamp === false ) {
+                       wfDebugLog( 'iptc', "IPTC: can't make final timestamp. Date: " . ( $unixTS + $tz ) );
+                       return null;
+               }
+               if ( $dateOnly ) {
+                       //return the date only
+                       return substr( $finalTimestamp, 0, 10 );
+               } else {
+                       return $finalTimestamp;
+               }
+
+       }
+
+       /**
+       * Helper function to convert charset for iptc values.
+       * @param $data Mixed String or Array: The iptc string
+       * @param $charset String: The charset
+       */
+       private static function convIPTC ( $data, $charset ) {
+               global $wgLang;
+               if ( is_array( $data ) ) {
+                       foreach ($data as &$val) {
+                               $val = self::convIPTCHelper( $val, $charset );
+                       }
+               } else {
+                       $data = self::convIPTCHelper ( $data, $charset );
+               }
+
+               return $data;
+       }
+       /**
+       * Helper function of a helper function to convert charset for iptc values.
+       * @param $data Mixed String or Array: The iptc string
+       * @param $charset String: The charset
+       */
+       private static function convIPTCHelper ( $data, $charset ) {
+               if ( $charset ) {
+                       $data = iconv($charset, "UTF-8//IGNORE", $data);
+                       if ($data === false) {
+                               $data = "";
+                               wfDebugLog('iptc', __METHOD__ . " Error converting iptc data charset $charset to utf-8");
+                       }
+               } else {
+                       //treat as utf-8 if is valid utf-8. otherwise pretend its windows-1252
+                       // most of the time if there is no 1:90 tag, it is either ascii, latin1, or utf-8
+                       $oldData = $data;
+                       UtfNormal::quickIsNFCVerify( $data ); //make $data valid utf-8
+                       if ($data === $oldData) return $data; //if validation didn't change $data
+                       else return self::convIPTCHelper ( $oldData, 'Windows-1252' );
+               }
+               return trim( $data );
+       }
+
+       /**
+       * take the value of 1:90 tag and returns a charset
+       * @param String $tag 1:90 tag. 
+       * @return charset name or "?"
+       * Warning, this function does not (and is not intended to) detect
+       * all iso 2022 escape codes. In practise, the code for utf-8 is the
+       * only code that seems to have wide use. It does detect that code.
+       */
+       static function getCharset($tag) {
+
+               //According to iim standard, charset is defined by the tag 1:90.
+               //in which there are iso 2022 escape sequences to specify the character set.
+               //the iim standard seems to encourage that all necessary escape sequences are
+               //in the 1:90 tag, but says it doesn't have to be.
+
+               //This is in need of more testing probably. This is definitely not complete.
+               //however reading the docs of some other iptc software, it appears that most iptc software
+               //only recognizes utf-8. If 1:90 tag is not present content is
+               // usually ascii or iso-8859-1 (and sometimes utf-8), but no guarantee.
+
+               //This also won't work if there are more than one escape sequence in the 1:90 tag
+               //or if something is put in the G2, or G3 charsets, etc. It will only reliably recognize utf-8.
+
+               // This is just going through the charsets mentioned in appendix C of the iim standard.
+
+               //  \x1b = ESC.
+               switch ( $tag ) {
+                       case "\x1b%G": //utf-8
+                       //Also call things that are compatible with utf-8, utf-8 (e.g. ascii)
+                       case "\x1b(B": // ascii
+                       case "\x1b(@": // iso-646-IRV (ascii in latest version, $ different in older version)
+                               $c = 'UTF-8';
+                               break;
+                       case "\x1b(A": //like ascii, but british.
+                               $c = 'ISO646-GB';
+                               break;
+                       case "\x1b(C": //some obscure sweedish/finland encoding
+                               $c = 'ISO-IR-8-1';
+                               break;
+                       case "\x1b(D":
+                               $c = 'ISO-IR-8-2';
+                               break;
+                       case "\x1b(E": //some obscure danish/norway encoding
+                               $c = 'ISO-IR-9-1';
+                               break;
+                       case "\x1b(F":
+                               $c = 'ISO-IR-9-2';
+                               break;
+                       case "\x1b(G":
+                               $c = 'SEN_850200_B'; // aka iso 646-SE; ascii-like
+                               break;
+                       case "\x1b(I":
+                               $c = "ISO646-IT";
+                               break;
+                       case "\x1b(L":
+                               $c = "ISO646-PT";
+                               break;
+                       case "\x1b(Z":
+                               $c = "ISO646-ES";
+                               break;
+                       case "\x1b([":
+                               $c = "GREEK7-OLD";
+                               break;
+                       case "\x1b(K":
+                               $c = "ISO646-DE";
+                               break;
+                       case "\x1b(N":  //crylic
+                               $c = "ISO_5427";
+                               break;
+                       case "\x1b(`": //iso646-NO
+                               $c = "NS_4551-1";
+                               break;
+                       case "\x1b(f": //iso646-FR
+                               $c = "NF_Z_62-010"; 
+                               break;
+                       case "\x1b(g":
+                               $c = "PT2"; //iso646-PT2
+                               break;
+                       case "\x1b(h":
+                               $c = "ES2";
+                               break;
+                       case "\x1b(i": //iso646-HU
+                               $c = "MSZ_7795.3";
+                               break;
+                       case "\x1b(w":
+                               $c = "CSA_Z243.4-1985-1";
+                               break;
+                       case "\x1b(x":
+                               $c = "CSA_Z243.4-1985-2";
+                               break;
+                       case "\x1b$(B":
+                       case "\x1b$B":
+                       case "\x1b&@\x1b$B":
+                       case "\x1b&@\x1b$(B":
+                               $c = "JIS_C6226-1983";
+                               break;
+                       case "\x1b-A": // iso-8859-1. at least for the high code characters.
+                       case "\x1b(@\x1b-A":
+                       case "\x1b(B\x1b-A":
+                               $c = 'ISO-8859-1';
+                               break;
+                       case "\x1b-B": // iso-8859-2. at least for the high code characters.
+                               $c = 'ISO-8859-2';
+                               break;
+                       case "\x1b-C": // iso-8859-3. at least for the high code characters.
+                               $c = 'ISO-8859-3';
+                               break;
+                       case "\x1b-D": // iso-8859-4. at least for the high code characters.
+                               $c = 'ISO-8859-4';
+                               break;
+                       case "\x1b-E": // iso-8859-5. at least for the high code characters.
+                               $c = 'ISO-8859-5';
+                               break;
+                       case "\x1b-F": // iso-8859-6. at least for the high code characters.
+                               $c = 'ISO-8859-6';
+                               break;
+                       case "\x1b-G": // iso-8859-7. at least for the high code characters.
+                               $c = 'ISO-8859-7';
+                               break;
+                       case "\x1b-H": // iso-8859-8. at least for the high code characters.
+                               $c = 'ISO-8859-8';
+                               break;
+                       case "\x1b-I": // CSN_369103. at least for the high code characters.
+                               $c = 'CSN_369103';
+                               break;
+                       default:
+                               wfDebugLog('iptc', __METHOD__ . 'Unknown charset in iptc 1:90: ' . bin2hex( $tag ) ); 
+                               //at this point just give up and refuse to parse iptc?
+                               $c = false;
+               }
+               return $c;
+       }
+}
diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php
new file mode 100644 (file)
index 0000000..c8a1043
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+/**
+ * @file
+ * @ingroup Media
+ */
+
+/** JPEG specific handler.
+ * Inherits most stuff from BitmapHandler, just here to do the metadata handler differently
+ * @ingroup Media
+ */
+class JpegHandler extends BitmapHandler {
+
+       const BROKEN_FILE = '-1'; // error extracting metadata
+       const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata.
+
+       function getMetadata ( $image, $filename ) {
+               try {
+                       $meta = BitmapMetadataHandler::Jpeg( $filename );
+                       if ( !is_array( $meta ) ) {
+                               // This should never happen, but doesn't hurt to be paranoid.
+                               throw new MWException('Metadata array is not an array');
+                       }
+                       $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version();
+                       return serialize( $meta );
+               }
+               catch ( MWException $e ) {
+                       // BitmapMetadataHandler throws an exception in certain exceptional cases like if file does not exist.
+                       wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+
+                       /* This used to use 0 (self::OLD_BROKEN_FILE) for the cases
+                        *      * No metadata in the file
+                        *      * Something is broken in the file.
+                        * However, if the metadata support gets expanded then you can't tell if the 0 is from
+                        * a broken file, or just no props found. A broken file is likely to stay broken, but
+                        * a file which had no props could have props once the metadata support is improved.
+                        * Thus switch to using -1 to denote only a broken file, and use an array with only
+                        * MEDIAWIKI_EXIF_VERSION to denote no props.
+                        */
+                       return self::BROKEN_FILE;
+               }
+       }
+
+       function convertMetadataVersion( $metadata, $version = 1 ) {
+               // basically flattens arrays.
+               $version = explode(';', $version, 2);
+               $version = intval($version[0]);
+               if ( $version < 1 || $version >= 2 ) {
+                       return $metadata;
+               }
+
+               $avoidHtml = true;
+
+               if ( !is_array( $metadata ) ) {
+                       $metadata = unserialize( $metadata );
+               }
+               if ( !isset( $metadata['MEDIAWIKI_EXIF_VERSION'] ) || $metadata['MEDIAWIKI_EXIF_VERSION'] != 2 ) {
+                       return $metadata;
+               }
+
+               // Treat Software as a special case because in can contain
+               // an array of (SoftwareName, Version).
+               if (isset( $metadata['Software'] ) 
+                       && is_array( $metadata['Software'] ) 
+                       && is_array( $metadata['Software'][0])
+                       && isset( $metadata['Software'][0][0] )
+                       && isset( $metadata['Software'][0][1])
+                ) {
+                       $metadata['Software'] = $metadata['Software'][0][0] . ' (Version '
+                               . $metadata['Software'][0][1] . ')';
+               }
+
+               // ContactInfo also has to be dealt with specially
+               if ( isset( $metadata['Contact'] ) ) {
+                       $metadata['Contact'] =
+                               FormatMetadata::collapseContactInfo(
+                                       $metadata['Contact'] );
+               }
+
+               foreach ( $metadata as &$val ) {
+                       if ( is_array( $val ) ) {
+                               $val = FormatMetadata::flattenArray( $val, 'ul', $avoidHtml );
+                       }
+               }
+               $metadata['MEDIAWIKI_EXIF_VERSION'] = 1;
+               return $metadata;
+       }
+
+       function isMetadataValid( $image, $metadata ) {
+               global $wgShowEXIF;
+               if ( !$wgShowEXIF ) {
+                       # Metadata disabled and so an empty field is expected
+                       return self::METADATA_GOOD;
+               }
+               if ( $metadata === self::OLD_BROKEN_FILE ) {
+                       # Old special value indicating that there is no EXIF data in the file.
+                       # or that there was an error well extracting the metadata.
+                       wfDebug( __METHOD__ . ": back-compat version\n");
+                       return self::METADATA_COMPATIBLE;
+               }
+               if ( $metadata === self::BROKEN_FILE ) {
+                       return self::METADATA_GOOD;
+               }
+               wfSuppressWarnings();
+               $exif = unserialize( $metadata );
+               wfRestoreWarnings();
+               if ( !isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) ||
+                       $exif['MEDIAWIKI_EXIF_VERSION'] != Exif::version() )
+               {
+                       if ( isset( $exif['MEDIAWIKI_EXIF_VERSION'] ) &&
+                               $exif['MEDIAWIKI_EXIF_VERSION'] == 1 )
+                       {
+                               //back-compatible but old
+                               wfDebug( __METHOD__.": back-compat version\n" );
+                               return self::METADATA_COMPATIBLE;
+                       }
+                       # Wrong (non-compatible) version
+                       wfDebug( __METHOD__.": wrong version\n" );
+                       return self::METADATA_BAD;
+               }
+               return self::METADATA_GOOD;
+       }
+
+       function formatMetadata( $image ) {
+               $metadata = $image->getMetadata();
+               if ( !$metadata || $metadata == self::BROKEN_FILE ) {
+                       return false;
+               }
+               $exif = unserialize( $metadata );
+               if ( !$exif ) {
+                       return false;
+               }
+               unset( $exif['MEDIAWIKI_EXIF_VERSION'] );
+               if ( count( $exif ) == 0 ) {
+                       return false;
+               }
+               return $this->formatMetadataHelper( $exif );
+       }
+
+
+}
diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php
new file mode 100644 (file)
index 0000000..d4f74f0
--- /dev/null
@@ -0,0 +1,212 @@
+<?php
+/**
+* Class for reading jpegs and extracting metadata.
+* see also BitmapMetadataHandler.
+*
+* Based somewhat on GIFMetadataExtrator.
+*/
+class JpegMetadataExtractor {
+       const MAX_JPEG_SEGMENTS = 200;
+       // the max segment is a sanity check.
+       // A jpeg file should never even remotely have
+       // that many segments. Your average file has about 10.
+
+       /** Function to extract metadata segments of interest from jpeg files
+       * based on GIFMetadataExtractor.
+       *
+       * we can almost use getimagesize to do this
+       * but gis doesn't support having multiple app1 segments
+       * and those can't extract xmp on files containing both exif and xmp data
+       *
+       * @param String $filename name of jpeg file
+       * @return Array of interesting segments.
+       * @throws MWException if given invalid file.
+       */
+       static function segmentSplitter ( $filename ) {
+               $showXMP = function_exists( 'xml_parser_create_ns' );
+
+               $segmentCount = 0;
+
+               $segments = Array( 'XMP_ext' => array(), 'COM' => array() );
+
+               if ( !$filename ) throw new MWException( "No filename specified for " . __METHOD__ );
+               if ( !file_exists( $filename ) || is_dir( $filename ) ) throw new MWException( "Invalid file $filename passed to " . __METHOD__ );
+
+               $fh = fopen( $filename, "rb" );
+
+               if ( !$fh ) throw new MWException( "Could not open file $filename" );
+
+               $buffer = fread( $fh, 2 );
+               if ( $buffer !== "\xFF\xD8" ) throw new MWException( "Not a jpeg, no SOI" );
+               while ( !feof( $fh ) ) {
+                       $buffer = fread( $fh, 1 );
+                       $segmentCount++;
+                       if ( $segmentCount > self::MAX_JPEG_SEGMENTS ) {
+                               // this is just a sanity check
+                               throw new MWException( 'Too many jpeg segments. Aborting' );
+                       }
+                       if ( $buffer !== "\xFF" ) {
+                               throw new MWException( "Error reading jpeg file marker" );
+                       }
+
+                       $buffer = fread( $fh, 1 );
+                       if ( $buffer === "\xFE" ) {
+
+                               // COM section -- file comment
+                               // First see if valid utf-8,
+                               // if not try to convert it to windows-1252.
+                               $com = $oldCom = trim( self::jpegExtractMarker( $fh ) );
+
+                               UtfNormal::quickIsNFCVerify( $com );
+                               // turns $com to valid utf-8.
+                               // thus if no change, its utf-8, otherwise its something else.
+                               if ( $com !== $oldCom ) {
+                                       $oldCom = iconv( 'windows-1252', 'UTF-8//IGNORE', $oldCom );
+                               }
+                               $segments["COM"][] = $oldCom;
+
+                       } elseif ( $buffer === "\xE1" && $showXMP ) {
+                               // APP1 section (Exif, XMP, and XMP extended)
+                               // only extract if XMP is enabled.
+                               $temp = self::jpegExtractMarker( $fh );
+
+                               // check what type of app segment this is.
+                               if ( substr( $temp, 0, 29 ) === "http://ns.adobe.com/xap/1.0/\x00" ) {
+                                       $segments["XMP"] = substr( $temp, 29 );
+                               } elseif ( substr( $temp, 0, 35 ) === "http://ns.adobe.com/xmp/extension/\x00" ) {
+                                       $segments["XMP_ext"][] = substr( $temp, 35 );
+                               } elseif ( substr( $temp, 0, 29 ) === "XMP\x00://ns.adobe.com/xap/1.0/\x00" ) {
+                                       // Some images (especially flickr images) seem to have this.
+                                       // I really have no idea what the deal is with them, but
+                                       // whatever...
+                                       $segments["XMP"] = substr( $temp, 29 );
+                                       wfDebug( __METHOD__ . ' Found XMP section with wrong app identifier '
+                                               . "Using anyways.\n" ); 
+                               }
+                       } elseif ( $buffer === "\xED" ) {
+                               // APP13 - PSIR. IPTC and some photoshop stuff
+                               $temp = self::jpegExtractMarker( $fh );
+                               if ( substr( $temp, 0, 14 ) === "Photoshop 3.0\x00" ) {
+                                       $segments["PSIR"] = $temp;
+                               }
+                       } elseif ( $buffer === "\xD9" || $buffer === "\xDA" ) {
+                               // EOI - end of image or SOS - start of scan. either way we're past any interesting segments
+                               return $segments;
+                       } else {
+                               // segment we don't care about, so skip
+                               $size = unpack( "nint", fread( $fh, 2 ) );
+                               if ( $size['int'] <= 2 ) throw new MWException( "invalid marker size in jpeg" );
+                               fseek( $fh, $size['int'] - 2, SEEK_CUR );
+                       }
+
+               }
+               // shouldn't get here.
+               throw new MWException( "Reached end of jpeg file unexpectedly" );
+
+       }
+       /**
+       * Helper function for jpegSegmentSplitter
+       * @param &$fh FileHandle for jpeg file
+       * @return data content of segment.
+       */
+       private static function jpegExtractMarker( &$fh ) {
+               $size = unpack( "nint", fread( $fh, 2 ) );
+               if ( $size['int'] <= 2 ) throw new MWException( "invalid marker size in jpeg" );
+               return fread( $fh, $size['int'] - 2 );
+       }
+
+       /**
+       * This reads the photoshop image resource.
+       * Currently it only compares the iptc/iim hash
+       * with the stored hash, which is used to determine the precedence
+       * of the iptc data. In future it may extract some other info, like
+       * url of copyright license.
+       *
+       * This should generally be called by BitmapMetadataHandler::doApp13()
+       *
+       * @param String $app13 photoshop psir app13 block from jpg.
+       * @return String if the iptc hash is good or not.
+       */
+       public static function doPSIR ( $app13 ) {
+               if ( !$app13 ) return;
+               // First compare hash with real thing
+               // 0x404 contains IPTC, 0x425 has hash
+               // This is used to determine if the iptc is newer than
+               // the xmp data, as xmp programs update the hash,
+               // where non-xmp programs don't.
+
+               $offset = 14; // skip past PHOTOSHOP 3.0 identifier. should already be checked.
+               $appLen = strlen( $app13 );
+               $realHash = "";
+               $recordedHash = "";
+
+               // the +12 is the length of an empty item.
+               while ( $offset + 12 <= $appLen ) {
+                       $valid = true;
+                       $id = false;
+                       $lenName = false;
+                       $lenData = false;
+
+                       if ( substr( $app13, $offset, 4 ) !== '8BIM' ) {
+                               // its supposed to be 8BIM
+                               // but apparently sometimes isn't esp. in
+                               // really old jpg's
+                               $valid = false;
+                       }
+                       $offset += 4;
+                       $id = substr( $app13, $offset, 2 );
+                       // id is a 2 byte id number which identifies
+                       // the piece of info this record contains.
+
+                       $offset += 2;
+
+                       // some record types can contain a name, which
+                       // is a pascal string 0-padded to be an even
+                       // number of bytes. Most times (and any time
+                       // we care) this is empty, making it two null bytes.
+
+                       $lenName = ord( substr( $app13, $offset, 1 ) ) + 1;
+                       // we never use the name so skip it. +1 for length byte
+                       if ( $lenName % 2 == 1 ) $lenName++; // pad to even.
+                       $offset += $lenName;
+
+                       // now length of data (unsigned long big endian)
+                       $lenData = unpack( 'Nlen', substr( $app13, $offset, 4 ) );
+                       $offset += 4; // 4bytes length field;
+
+                       // this should not happen, but check.
+                       if ( $lenData['len'] + $offset > $appLen ) {
+                               wfDebug( __METHOD__ . " PSIR data too long.\n" );
+                               return 'iptc-no-hash';
+                       }
+
+                       if ( $valid ) {
+                               switch ( $id ) {
+                                       case "\x04\x04":
+                                               // IPTC block
+                                               $realHash = md5( substr( $app13, $offset, $lenData['len'] ), true );
+                                               break;
+                                       case "\x04\x25":
+                                               $recordedHash = substr( $app13, $offset, $lenData['len'] );
+                                               break;
+                               }
+                       }
+
+                       // if odd, add 1 to length to account for
+                       // null pad byte.
+                       if ( $lenData['len'] % 2 == 1 ) $lenData['len']++;
+                       $offset += $lenData['len'];
+
+               }
+
+               if ( !$realHash || !$recordedHash ) {
+                       return 'iptc-no-hash';
+               } elseif ( $realHash === $recordedHash ) {
+                       return 'iptc-good-hash';
+               } else { /*$realHash !== $recordedHash */
+                       return 'iptc-bad-hash';
+               }
+
+       }
+
+}
index 3f0f7ea..3206d96 100644 (file)
  */
 class PNGHandler extends BitmapHandler {
 
+       const BROKEN_FILE = '0';
+       
+
        /**
         * @param File $image
         * @param string $filename
         * @return string
         */
        function getMetadata( $image, $filename ) {
-               if ( !isset($image->parsedPNGMetadata) ) {
-                       try {
-                               $image->parsedPNGMetadata = PNGMetadataExtractor::getMetadata( $filename );
-                       } catch( Exception $e ) {
-                               // Broken file?
-                               wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
-                               return '0';
-                       }
+               try {
+                       $metadata = BitmapMetadataHandler::PNG( $filename );
+               } catch( Exception $e ) {
+                       // Broken file?
+                       wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+                       return self::BROKEN_FILE;
                }
 
-               return serialize($image->parsedPNGMetadata);
-
+               return serialize($metadata);
        }
        
        function formatMetadata( $image ) {
-               return false;
+               $meta = $image->getMetadata();
+
+               if ( !$meta ) {
+                       return false;
+               }
+               $meta = unserialize( $meta );
+               if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) {
+                       return false;
+               }
+
+               if ( isset( $meta['metadata']['_MW_PNG_VERSION'] ) ) {
+                       unset( $meta['metadata']['_MW_PNG_VERSION'] );
+               }
+               return $this->formatMetadataHelper( $meta['metadata'] );
        }
 
        /**
@@ -55,10 +68,27 @@ class PNGHandler extends BitmapHandler {
        }
        
        function isMetadataValid( $image, $metadata ) {
+
+               if ( $metadata === self::BROKEN_FILE ) {
+                       // Do not repetitivly regenerate metadata on broken file.
+                       return self::METADATA_GOOD;
+               }
+
                wfSuppressWarnings();
                $data = unserialize( $metadata );
                wfRestoreWarnings();
-               return (boolean) $data;
+
+               if ( !$data || !is_array( $data ) ) {
+                       wfDebug(__METHOD__ . ' invalid png metadata' );
+                       return self::METADATA_BAD;
+               }
+
+               if ( !isset( $data['metadata']['_MW_PNG_VERSION'] )
+                       || $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION ) {
+                       wfDebug(__METHOD__ . ' old but compatible png metadata' );
+                       return self::METADATA_COMPATIBLE;
+               }
+               return self::METADATA_GOOD;
        }
 
        /**
index 56e6ee9..e63a280 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * PNG frame counter.
+ * PNG frame counter and metadata extractor.
  * Slightly derived from GIFMetadataExtractor.php
  * Deliberately not using MWExceptions to avoid external dependencies, encouraging
  * redistribution.
 class PNGMetadataExtractor {
        static $png_sig;
        static $CRC_size;
+       static $text_chunks;
+
+       const VERSION = 1;
+       const MAX_CHUNK_SIZE = 3145728; // 3 megabytes
 
        static function getMetadata( $filename ) {
                self::$png_sig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 );
                self::$CRC_size = 4;
-               
+               /* based on list at http://owl.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData
+                * and http://www.w3.org/TR/PNG/#11keywords
+                */
+               self::$text_chunks = array(
+                       'xml:com.adobe.xmp' => 'xmp',
+                       # Artist is unofficial. Author is the recommended
+                       # keyword in the PNG spec. However some people output
+                       # Artist so support both.
+                       'artist'      => 'Artist',
+                       'model'       => 'Model',
+                       'make'        => 'Make',
+                       'author'      => 'Artist',
+                       'comment'     => 'PNGFileComment',
+                       'description' => 'ImageDescription',
+                       'title'       => 'ObjectName',
+                       'copyright'   => 'Copyright',
+                       # Source as in original device used to make image
+                       # not as in who gave you the image
+                       'source'      => 'Model',
+                       'software'    => 'Software',
+                       'disclaimer'  => 'Disclaimer',
+                       'warning'     => 'ContentWarning',
+                       'url'         => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement.
+                       'label'       => 'Label',
+                       'creation time' => 'DateTimeDigitized',
+                       /* Other potentially useful things - Document */
+               );
+
+               $showXMP = function_exists( 'xml_parser_create_ns' );
+
                $frameCount = 0;
                $loopCount = 1;
-               $duration = 0.0;
+               $text = array();
                $bitDepth = 0;
                $colorType = 'unknown';
 
-               if (!$filename)
+               if ( !$filename ) {
                        throw new Exception( __METHOD__ . ": No file name specified" );
-               elseif ( !file_exists($filename) || is_dir($filename) )
+               } elseif ( !file_exists( $filename ) || is_dir( $filename ) ) {
                        throw new Exception( __METHOD__ . ": File $filename does not exist" );
-               
+               }
+
                $fh = fopen( $filename, 'r' );
-               
-               if (!$fh) {
+
+               if ( !$fh ) {
                        throw new Exception( __METHOD__ . ": Unable to open file $filename" );
                }
-               
+
                // Check for the PNG header
                $buf = fread( $fh, 8 );
                if ( $buf != self::$png_sig ) {
@@ -46,22 +80,22 @@ class PNGMetadataExtractor {
                }
 
                // Read chunks
-               while( !feof( $fh ) ) {
+               while ( !feof( $fh ) ) {
                        $buf = fread( $fh, 4 );
-                       if( !$buf ) {
+                       if ( !$buf ) {
                                throw new Exception( __METHOD__ . ": Read error" );
                        }
-                       $chunk_size = unpack( "N", $buf);
+                       $chunk_size = unpack( "N", $buf );
                        $chunk_size = $chunk_size[1];
 
                        $chunk_type = fread( $fh, 4 );
-                       if( !$chunk_type ) {
+                       if ( !$chunk_type ) {
                                throw new Exception( __METHOD__ . ": Read error" );
                        }
 
                        if ( $chunk_type == "IHDR" ) {
-                               $buf = fread( $fh, $chunk_size );
-                               if( !$buf ) {
+                               $buf = self::read( $fh, $chunk_size );
+                               if ( !$buf ) {
                                        throw new Exception( __METHOD__ . ": Read error" );
                                }
                                $bitDepth = ord( substr( $buf, 8, 1 ) );
@@ -97,20 +131,203 @@ class PNGMetadataExtractor {
                                $frameCount = $actl['frames'];
                                $loopCount = $actl['plays'];
                        } elseif ( $chunk_type == "fcTL" ) {
-                               $buf = fread( $fh, $chunk_size );
-                               if( !$buf ) {
+                               $buf = self::read( $fh, $chunk_size );
+                               if ( !$buf ) {
                                        throw new Exception( __METHOD__ . ": Read error" );
                                }
-                               $buf = substr( $buf, 20 );      
+                               $buf = substr( $buf, 20 );
 
                                $fctldur = unpack( "ndelay_num/ndelay_den", $buf );
-                               if( $fctldur['delay_den'] == 0 ) $fctldur['delay_den'] = 100;
-                               if( $fctldur['delay_num'] ) {
+                               if ( $fctldur['delay_den'] == 0 ) {
+                                       $fctldur['delay_den'] = 100;
+                               }
+                               if ( $fctldur['delay_num'] ) {
                                        $duration += $fctldur['delay_num'] / $fctldur['delay_den'];
                                }
-                       } elseif ( ( $chunk_type == "IDAT" || $chunk_type == "IEND" ) && $frameCount == 0 ) {
-                               // Not a valid animated image. No point in continuing.
-                               break;
+                       } elseif ( $chunk_type == "iTXt" ) {
+                               // Extracts iTXt chunks, uncompressing if necessary.
+                               $buf = self::read( $fh, $chunk_size );
+                               $items = array();
+                               if ( preg_match(
+                                       '/^([^\x00]{1,79})\x00(\x00|\x01)\x00([^\x00]*)(.)[^\x00]*\x00(.*)$/Ds',
+                                       $buf, $items )
+                               ) {
+                                       /* $items[1] = text chunk name, $items[2] = compressed flag,
+                                        * $items[3] = lang code (or ""), $items[4]= compression type.
+                                        * $items[5] = content
+                                        */
+
+                                       // Theoretically should be case-sensitive, but in practise...
+                                       $items[1] = strtolower( $items[1] );
+                                       if ( !isset( self::$text_chunks[$items[1]] ) ) {
+                                               // Only extract textual chunks on our list.
+                                               fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                               continue;
+                                       }
+
+                                       $items[3] = strtolower( $items[3] );
+                                       if ( $items[3] == '' ) {
+                                               // if no lang specified use x-default like in xmp.
+                                               $items[3] = 'x-default';
+                                       }
+
+                                       // if compressed
+                                       if ( $items[2] == "\x01" ) {
+                                               if ( function_exists( 'gzuncompress' ) && $items[4] === "\x00" ) {
+                                                       wfSuppressWarnings();
+                                                       $items[5] = gzuncompress( $items[5] );
+                                                       wfRestoreWarnings();
+
+                                                       if ( $items[5] === false ) {
+                                                               // decompression failed
+                                                               wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] );
+                                                               fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                                               continue;
+                                                       }
+
+                                               } else {
+                                                       wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,'
+                                                               . ' or potentially invalid compression method' );
+                                                       fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                                       continue;
+                                               }
+                                       }
+                                       $finalKeyword = self::$text_chunks[ $items[1] ];
+                                       $text[ $finalKeyword ][ $items[3] ] = $items[5];
+                                       $text[ $finalKeyword ]['_type'] = 'lang';
+
+                               } else {
+                                       // Error reading iTXt chunk
+                                       throw new Exception( __METHOD__ . ": Read error on iTXt chunk" );
+                               }
+
+                       } elseif ( $chunk_type == 'tEXt' ) {
+                               $buf = self::read( $fh, $chunk_size );
+                               $keyword = '';
+                               $content = '';
+
+                               list( $keyword, $content ) = explode( "\x00", $buf, 2 );
+                               if ( $keyword === '' || $content === '' ) {
+                                       throw new Exception( __METHOD__ . ": Read error on tEXt chunk" );
+                               }
+
+                               // Theoretically should be case-sensitive, but in practise...
+                               $keyword = strtolower( $keyword );
+                               if ( !isset( self::$text_chunks[ $keyword ] ) ) {
+                                       // Don't recognize chunk, so skip.
+                                       fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                       continue;
+                               }
+                               wfSuppressWarnings();
+                               $content = iconv( 'ISO-8859-1', 'UTF-8', $content );
+                               wfRestoreWarnings();
+
+                               if ( $content === false ) {
+                                       throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
+                               }
+
+                               $finalKeyword = self::$text_chunks[ $keyword ];
+                               $text[ $finalKeyword ][ 'x-default' ] = $content;
+                               $text[ $finalKeyword ]['_type'] = 'lang';
+
+                       } elseif ( $chunk_type == 'zTXt' ) {
+                               if ( function_exists( 'gzuncompress' ) ) {
+                                       $buf = self::read( $fh, $chunk_size );
+                                       $keyword = '';
+                                       $postKeyword = '';
+
+                                       list( $keyword, $postKeyword ) = explode( "\x00", $buf, 2 );
+                                       if ( $keyword === '' || $postKeyword === '' ) {
+                                               throw new Exception( __METHOD__ . ": Read error on zTXt chunk" );
+                                       }
+                                       // Theoretically should be case-sensitive, but in practise...
+                                       $keyword = strtolower( $keyword );
+
+                                       if ( !isset( self::$text_chunks[ $keyword ] ) ) {
+                                               // Don't recognize chunk, so skip.
+                                               fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                               continue;
+                                       }
+                                       $compression = substr( $postKeyword, 0, 1 );
+                                       $content = substr( $postKeyword, 1 );
+                                       if ( $compression !== "\x00" ) {
+                                               wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping." );
+                                               fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                               continue;
+                                       }
+
+                                       wfSuppressWarnings();
+                                       $content = gzuncompress( $content );
+                                       wfRestoreWarnings();
+
+                                       if ( $content === false ) {
+                                               // decompression failed
+                                               wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword );
+                                               fseek( $fh, self::$CRC_size, SEEK_CUR );
+                                               continue;
+                                       }
+
+                                       wfSuppressWarnings();
+                                       $content = iconv( 'ISO-8859-1', 'UTF-8', $content );
+                                       wfRestoreWarnings();
+
+                                       if ( $content === false ) {
+                                               throw new Exception( __METHOD__ . ": Read error (error with iconv)" );
+                                       }
+
+                                       $finalKeyword = self::$text_chunks[ $keyword ];
+                                       $text[ $finalKeyword ][ 'x-default' ] = $content;
+                                       $text[ $finalKeyword ]['_type'] = 'lang';
+
+                               } else {
+                                       wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping." );
+                                       fseek( $fh, $chunk_size, SEEK_CUR );
+                               }
+                       } elseif ( $chunk_type == 'tIME' ) {
+                               // last mod timestamp.
+                               if ( $chunk_size !== 7 ) {
+                                       throw new Exception( __METHOD__ . ": tIME wrong size" );
+                               }
+                               $buf = self::read( $fh, $chunk_size );
+                               if ( !$buf ) {
+                                       throw new Exception( __METHOD__ . ": Read error" );
+                               }
+
+                               // Note: spec says this should be UTC.
+                               $t = unpack( "ny/Cm/Cd/Ch/Cmin/Cs", $buf );
+                               $strTime = sprintf( "%04d%02d%02d%02d%02d%02d",
+                                       $t['y'], $t['m'], $t['d'], $t['h'],
+                                       $t['min'], $t['s'] );
+
+                               $exifTime = wfTimestamp( TS_EXIF, $strTime );
+
+                               if ( $exifTime ) {
+                                       $text['DateTime'] = $exifTime;
+                               }
+
+                       } elseif ( $chunk_type == 'pHYs' ) {
+                               // how big pixels are (dots per meter).
+                               if ( $chunk_size !== 9 ) {
+                                       throw new Exception( __METHOD__ . ": pHYs wrong size" );
+                               }
+
+                               $buf = self::read( $fh, $chunk_size );
+                               if ( !$buf ) {
+                                       throw new Exception( __METHOD__ . ": Read error" );
+                               }
+
+                               $dim = unpack( "Nwidth/Nheight/Cunit", $buf );
+                               if ( $dim['unit'] == 1 ) {
+                                       // unit is meters
+                                       // (as opposed to 0 = undefined )
+                                       $text['XResolution'] = $dim['width']
+                                               . '/100';
+                                       $text['YResolution'] = $dim['height']
+                                               . '/100';
+                                       $text['ResolutionUnit'] = 3;
+                                       // 3 = dots per cm (from Exif).
+                               }
+
                        } elseif ( $chunk_type == "IEND" ) {
                                break;
                        } else {
@@ -120,17 +337,60 @@ class PNGMetadataExtractor {
                }
                fclose( $fh );
 
-               if( $loopCount > 1 ) {
+               if ( $loopCount > 1 ) {
                        $duration *= $loopCount;
                }
 
+               if ( isset( $text['DateTimeDigitized'] ) ) {
+                       // Convert date format from rfc2822 to exif.
+                       foreach ( $text['DateTimeDigitized'] as $name => &$value ) {
+                               if ( $name === '_type' ) {
+                                       continue;
+                               }
+
+                               // fixme: currently timezones are ignored.
+                               // possibly should be wfTimestamp's
+                               // responsibility. (at least for numeric TZ)
+                               $formatted = wfTimestamp( TS_EXIF, $value );
+                               if ( $formatted ) {
+                                       // Only change if we could convert the
+                                       // date.
+                                       // The png standard says it should be
+                                       // in rfc2822 format, but not required.
+                                       // In general for the exif stuff we
+                                       // prettify the date if we can, but we
+                                       // display as-is if we cannot or if
+                                       // it is invalid.
+                                       // So do the same here.
+
+                                       $value = $formatted;
+                               }
+                       }
+               }
                return array(
                        'frameCount' => $frameCount,
                        'loopCount' => $loopCount,
                        'duration' => $duration,
+                       'text' => $text,
+                       'duration' => $duration,
                        'bitDepth' => $bitDepth,
                        'colorType' => $colorType,
                );
-               
+
+       }
+       /**
+        * Read a chunk, checking to make sure its not too big.
+        *
+        * @param $fh resource The file handle
+        * @param $size Integer size in bytes.
+        * @throws Exception if too big.
+        * @return String The chunk.
+        */
+       static private function read( $fh, $size ) {
+               if ( $size > self::MAX_CHUNK_SIZE ) {
+                       throw new Exception( __METHOD__ . ': Chunk size of ' . $size .
+                               ' too big. Max size is: ' . self::MAX_CHUNK_SIZE );
+               }
+               return fread( $fh, $size );
        }
 }
diff --git a/includes/media/XMP.php b/includes/media/XMP.php
new file mode 100644 (file)
index 0000000..dc56e80
--- /dev/null
@@ -0,0 +1,1165 @@
+<?php
+/**
+* Class for reading xmp data containing properties relevant to
+* images, and spitting out an array that FormatExif accepts.
+*
+* Note, this is not meant to recognize every possible thing you can
+* encode in XMP. It should recognize all the properties we want.
+* For example it doesn't have support for structures with multiple
+* nesting levels, as none of the properties we're supporting use that
+* feature. If it comes across properties it doesn't recognize, it should
+* ignore them.
+*
+* The public methods one would call in this class are
+* - parse( $content )
+*      Reads in xmp content.
+*      Can potentially be called multiple times with partial data each time.
+* - parseExtended( $content )
+*      Reads XMPExtended blocks (jpeg files only).
+* - getResults
+*      Outputs a results array.
+*
+* Note XMP kind of looks like rdf. They are not the same thing - XMP is
+* encoded as a specific subset of rdf. This class can read XMP. It cannot
+* read rdf.
+*
+*/
+class XMPReader {
+
+       private $curItem = array();        // array to hold the current element (and previous element, and so on)
+       private $ancestorStruct = false;   // the structure name when processing nested structures.
+       private $charContent = false;      // temporary holder for character data that appears in xmp doc.
+       private $mode = array();           // stores the state the xmpreader is in (see MODE_FOO constants)
+       private $results = array();        // array to hold results
+       private $processingArray = false;  // if we're doing a seq or bag.
+       private $itemLang = false;         // used for lang alts only
+
+       private $xmlParser;
+       private $charset = false;
+       private $extendedXMPOffset = 0;
+
+       protected $items;
+
+       /*
+       * These are various mode constants.
+       * they are used to figure out what to do
+       * with an element when its encountered.
+       *
+       * For example, MODE_IGNORE is used when processing
+       * a property we're not interested in. So if a new
+       * element pops up when we're in that mode, we ignore it.
+       */
+       const MODE_INITIAL = 0;
+       const MODE_IGNORE  = 1;
+       const MODE_LI      = 2;
+       const MODE_LI_LANG = 3;
+       const MODE_QDESC   = 4;
+
+       // The following MODE constants are also used in the
+       // $items array to denote what type of property the item is.
+       const MODE_SIMPLE    = 10;
+       const MODE_STRUCT    = 11; // structure (associative array)
+       const MODE_SEQ       = 12; // ordered list
+       const MODE_BAG       = 13; // unordered list
+       const MODE_LANG      = 14;
+       const MODE_ALT       = 15; // non-language alt. Currently not implemented, and not needed atm.
+       const MODE_BAGSTRUCT = 16; // A BAG of Structs.
+
+       const NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
+       const NS_XML = 'http://www.w3.org/XML/1998/namespace';
+
+
+       /**
+       * Constructor.
+       *
+       * Primary job is to initialize the XMLParser
+       */
+       function __construct() {
+
+               if ( !function_exists( 'xml_parser_create_ns' ) ) {
+                       // this should already be checked by this point
+                       throw new MWException( 'XMP support requires XML Parser' );
+               }
+
+               $this->items = XMPInfo::getItems();
+
+               $this->resetXMLParser();
+
+       }
+       /**
+       * Main use is if a single item has multiple xmp documents describing it.
+       * For example in jpeg's with extendedXMP
+       */
+       private function resetXMLParser() {
+
+               if ($this->xmlParser) {
+                       //is this needed?
+                       xml_parser_free( $this->xmlParser );
+               }
+
+               $this->xmlParser = xml_parser_create_ns( 'UTF-8', ' ' );
+               xml_parser_set_option( $this->xmlParser, XML_OPTION_CASE_FOLDING, 0 );
+               xml_parser_set_option( $this->xmlParser, XML_OPTION_SKIP_WHITE, 1 );
+
+               xml_set_element_handler( $this->xmlParser,
+                       array( $this, 'startElement' ),
+                       array( $this, 'endElement' ) );
+
+               xml_set_character_data_handler( $this->xmlParser, array( $this, 'char' ) );
+
+
+       }
+
+       /** Destroy the xml parser
+       *
+       * Not sure if this is actually needed.
+       */
+       function __destruct() {
+               // not sure if this is needed.
+               xml_parser_free( $this->xmlParser );
+       }
+
+       /** Get the result array. Do some post-processing before returning
+       * the array, and transform any metadata that is special-cased.
+       *
+       * @return Array array of results as an array of arrays suitable for
+       *       FormatMetadata::getFormattedData().
+       */
+       public function getResults() {
+               // xmp-special is for metadata that affects how stuff
+               // is extracted. For example xmpNote:HasExtendedXMP.
+
+               // It is also used to handle photoshop:AuthorsPosition
+               // which is weird and really part of another property,
+               // see 2:85 in IPTC. See also pg 21 of IPTC4XMP standard.
+               // The location fields also use it.
+
+               $data = $this->results;
+
+               wfRunHooks('XMPGetResults', Array(&$data));
+
+               if ( isset( $data['xmp-special']['AuthorsPosition'] )
+                       && is_string( $data['xmp-special']['AuthorsPosition'] )
+                       && isset( $data['xmp-general']['Artist'][0] )
+               ) {
+                       // Note, if there is more than one creator,
+                       // this only applies to first. This also will
+                       // only apply to the dc:Creator prop, not the
+                       // exif:Artist prop.
+
+                       $data['xmp-general']['Artist'][0] = 
+                               $data['xmp-special']['AuthorsPosition'] . ', '
+                               . $data['xmp-general']['Artist'][0];
+               }
+
+               // Go through the LocationShown and LocationCreated
+               // changing it to the non-hierarchal form used by
+               // the other location fields.
+
+               if ( isset( $data['xmp-special']['LocationShown'][0] )
+                       && is_array( $data['xmp-special']['LocationShown'][0] )
+               ) {
+                       // the is_array is just paranoia. It should always
+                       // be an array.
+                       foreach( $data['xmp-special']['LocationShown'] as $loc ) {
+                               if ( !is_array( $loc ) ) {
+                                       // To avoid copying over the _type meta-fields.
+                                       continue;
+                               }
+                               foreach( $loc as $field => $val ) {
+                                       $data['xmp-general'][$field . 'Dest'][] = $val;
+                               }
+                       }
+               }
+               if ( isset( $data['xmp-special']['LocationCreated'][0] )
+                       && is_array( $data['xmp-special']['LocationCreated'][0] )
+               ) {
+                       // the is_array is just paranoia. It should always
+                       // be an array.
+                       foreach( $data['xmp-special']['LocationCreated'] as $loc ) {
+                               if ( !is_array( $loc ) ) {
+                                       // To avoid copying over the _type meta-fields.
+                                       continue;
+                               }
+                               foreach(  $loc as $field => $val ) {
+                                       $data['xmp-general'][$field . 'Created'][] = $val;
+                               }
+                       }
+               }
+
+
+               // We don't want to return the special values, since they're
+               // special and not info to be stored about the file.
+               unset( $data['xmp-special'] );
+
+               // Convert GPSAltitude to negative if below sea level.
+               if ( isset( $data['xmp-exif']['GPSAltitudeRef'] ) ) {
+                       if ( $data['xmp-exif']['GPSAltitudeRef'] == '1'
+                               && isset( $data['xmp-exif']['GPSAltitude'] )
+                       ) {
+                               $data['xmp-exif']['GPSAltitude'] *= -1;
+                       }
+                       unset( $data['xmp-exif']['GPSAltitudeRef'] );
+               }
+
+               return $data;
+       }
+
+       /**
+       * Main function to call to parse XMP. Use getResults to
+       * get results.
+       *
+       * Also catches any errors during processing, writes them to
+       * debug log, blanks result array and returns false.
+       *
+       * @param String: $content XMP data
+       * @param Boolean: $allOfIt If this is all the data (true) or if its split up (false). Default true
+       * @param Boolean: $reset - does xml parser need to be reset. Default false
+       * @return Boolean success.
+       */
+       public function parse( $content, $allOfIt = true, $reset = false ) {
+               if ( $reset ) {
+                       $this->resetXMLParser();
+               }
+               try {
+
+                       // detect encoding by looking for BOM which is supposed to be in processing instruction.
+                       // see page 12 of http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart3.pdf
+                       if ( !$this->charset ) {
+                               $bom = array();
+                               if ( preg_match( '/\xEF\xBB\xBF|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\xFF\xFE/',
+                                        $content, $bom ) 
+                               ) {
+                                       switch ( $bom[0] ) {
+                                               case "\xFE\xFF":
+                                                       $this->charset = 'UTF-16BE';
+                                                       break;
+                                               case "\xFF\xFE":
+                                                       $this->charset = 'UTF-16LE';
+                                                       break;
+                                               case "\x00\x00\xFE\xFF":
+                                                       $this->charset = 'UTF-32BE';
+                                                       break;
+                                               case "\xFF\xFE\x00\x00":
+                                                       $this->charset = 'UTF-32LE';
+                                                       break;
+                                               case "\xEF\xBB\xBF":
+                                                       $this->charset = 'UTF-8';
+                                                       break;
+                                               default:
+                                                       //this should be impossible to get to
+                                                       throw new MWException("Invalid BOM");
+                                                       break;
+
+                                       }
+
+                               } else {
+                                       // standard specifically says, if no bom assume utf-8
+                                       $this->charset = 'UTF-8';
+                               }
+                       }
+                       if ( $this->charset !== 'UTF-8' ) {
+                               //don't convert if already utf-8
+                               $content = iconv( $this->charset, 'UTF-8//IGNORE', $content );
+                       }
+
+                       $ok = xml_parse( $this->xmlParser, $content, $allOfIt );
+                       if ( !$ok ) {
+                               $error = xml_error_string( xml_get_error_code( $this->xmlParser ) );
+                               $where = 'line: ' . xml_get_current_line_number( $this->xmlParser )
+                                       . ' column: ' . xml_get_current_column_number( $this->xmlParser )
+                                       . ' byte offset: ' . xml_get_current_byte_index( $this->xmlParser );
+
+                               wfDebugLog( 'XMP', "XMPReader::parse : Error reading XMP content: $error ($where)" );
+                               $this->results = array(); // blank if error.
+                               return false;
+                       }
+               } catch ( MWException $e ) {
+                       wfDebugLog( 'XMP', 'XMP parse error: ' . $e );
+                       $this->results = array();
+                       return false;
+               }
+               return true;
+       }
+
+       /** Entry point for XMPExtended blocks in jpeg files
+        *
+        * @todo In serious need of testing
+        * @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20
+        * @param String $content XMPExtended block minus the namespace signature
+        * @return Boolean If it succeeded.
+        */
+       public function parseExtended( $content ) {
+               // FIXME: This is untested. Hard to find example files
+               // or programs that make such files..
+               $guid = substr( $content, 0, 32 );
+               if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] )
+                       || $this->results['xmp-special']['HasExtendedXMP'] !== $guid )
+               {
+                       wfDebugLog('XMP', __METHOD__ . " Ignoring XMPExtended block due to wrong guid (guid= '$guid' )");
+                       return;
+               }
+               $len  = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) );
+
+               if (!$len || $len['length'] < 4 || $len['offset'] < 0 || $len['offset'] > $len['length'] ) {
+                       wfDebugLog('XMP', __METHOD__ . 'Error reading extended XMP block, invalid length or offset.');
+                       return false;
+               }
+
+
+               // we're not very robust here. we should accept it in the wrong order. To quote
+               // the xmp standard:
+               // "A JPEG writer should write the ExtendedXMP marker segments in order, immediately following the
+               // StandardXMP. However, the JPEG standard does not require preservation of marker segment order. A
+               // robust JPEG reader should tolerate the marker segments in any order."
+               //
+               // otoh the probability that an image will have more than 128k of metadata is rather low...
+               // so the probability that it will have > 128k, and be in the wrong order is very low...
+
+               if ( $len['offset'] !== $this->extendedXMPOffset ) {
+                       wfDebugLog('XMP', __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was '
+                               . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')');
+                       return false;
+               }
+
+               if ( $len['offset'] === 0 ) {
+                       // if we're starting the extended block, we've probably already
+                       // done the XMPStandard block, so reset.
+                       $this->resetXMLParser();
+               }
+
+               $this->extendedXMPOffset += $len['length'];
+
+               $actualContent = substr( $content, 40 );
+
+               if ( $this->extendedXMPOffset === strlen( $actualContent ) ) {
+                       $atEnd = true;
+               } else {
+                       $atEnd = false;
+               }
+
+               wfDebugLog('XMP', __METHOD__ . 'Parsing a XMPExtended block');
+               return $this->parse( $actualContent, $atEnd );
+       }
+
+       /**
+       * Character data handler
+       * Called whenever character data is found in the xmp document.
+       *
+       * does nothing if we're in MODE_IGNORE or if the data is whitespace
+       * throws an error if we're not in MODE_SIMPLE (as we're not allowed to have character
+       * data in the other modes).
+       *
+       * As an example, this happens when we encounter XMP like:
+       * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+       * and are processing the 0/10 bit.
+       *
+       * @param $parser XMLParser reference to the xml parser
+       * @param $data String Character data
+       * @throws MWException on invalid data
+       */
+       function char( $parser, $data ) {
+
+               $data = trim( $data );
+               if ( trim( $data ) === "" ) {
+                       return;
+               }
+
+               if ( !isset( $this->mode[0] ) ) {
+                       throw new MWException( 'Unexpected character data before first rdf:Description element' );
+               }
+
+               if ( $this->mode[0] === self::MODE_IGNORE ) return;
+
+               if ( $this->mode[0] !== self::MODE_SIMPLE
+                       && $this->mode[0] !== self::MODE_QDESC
+               ) {
+                       throw new MWException( 'character data where not expected. (mode ' . $this->mode[0] . ')' );
+               }
+
+               // to check, how does this handle w.s.
+               if ( $this->charContent === false ) {
+                       $this->charContent = $data;
+               } else {
+                       $this->charContent .= $data;
+               }
+
+       }
+       /** When we hit a closing element in MODE_IGNORE
+       * Check to see if this is the element we started to ignore,
+       * in which case we get out of MODE_IGNORE
+       *
+       * @param $elm String Namespace of element followed by a space and then tag name of element.
+       */
+       private function endElementModeIgnore ( $elm ) {
+
+               if ( $this->curItem[0] === $elm ) {
+                       array_shift( $this->curItem );
+                       array_shift( $this->mode );
+               }
+               return;
+
+       }
+       /**
+       * Hit a closing element when in MODE_SIMPLE.
+       * This generally means that we finished processing a
+       * property value, and now have to save the result to the
+       * results array
+       *
+       * For example, when processing: 
+       * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio>
+       * this deals with when we hit </exif:DigitalZoomRatio>.
+       *
+       * Or it could be if we hit the end element of a property
+       * of a compound data structure (like a member of an array).
+       *
+       * @param $elm String namespace, space, and tag name.
+       */
+       private function endElementModeSimple ( $elm ) {
+               if ( $this->charContent !== false ) {
+                       if ( $this->processingArray ) {
+                               // if we're processing an array, use the original element
+                               // name instead of rdf:li.
+                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                       } else {
+                               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+                       }
+                       $this->saveValue( $ns, $tag, $this->charContent );
+
+                       $this->charContent = false; // reset
+               }
+               array_shift( $this->curItem );
+               array_shift( $this->mode );
+
+       }
+       /**
+       * Hit a closing element in MODE_STRUCT, MODE_SEQ, MODE_BAG
+       * generally means we've finished processing a nested structure.
+       * resets some internal variables to indicate that.
+       *
+       * Note this means we hit the </closing element> not the </rdf:Seq>.
+       *
+       * For example, when processing:
+       * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+       *   </rdf:Seq> </exif:ISOSpeedRatings>
+       *
+       * This method is called when we hit the </exif:ISOSpeedRatings> tag.
+       *
+       * @param $elm String namespace . space . tag name.
+       */
+       private function endElementNested( $elm ) {
+
+               /* cur item must be the same as $elm, unless if in MODE_STRUCT
+                  in which case it could also be rdf:Description */
+               if ( $this->curItem[0] !== $elm
+                       && !( $elm === self::NS_RDF . ' Description'
+                               && $this->mode[0] === self::MODE_STRUCT )
+                ) {
+                       throw new MWException( "nesting mismatch. got a </$elm> but expected a </" . $this->curItem[0] . '>' );
+               }
+
+               // Validate structures.
+               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+               if ( isset( $this->items[$ns][$tag]['validate'] ) ) {
+
+                       $info =& $this->items[$ns][$tag];
+                       $finalName = isset( $info['map_name'] )
+                               ? $info['map_name'] : $tag;
+
+                       $validate = is_array( $info['validate'] ) ? $info['validate']
+                               : array( 'XMPValidate', $info['validate'] );
+
+                       if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
+                               // This can happen if all the members of the struct failed validation.
+                               wfDebugLog( 'XMP', __METHOD__ . " <$ns:$tag> has no valid members." );
+
+                       } elseif ( is_callable( $validate ) ) {
+                               $val =& $this->results['xmp-' . $info['map_group']][$finalName];
+                               call_user_func_array( $validate, array( $info, &$val, false ) );
+                               if ( is_null( $val ) ) {
+                                       // the idea being the validation function will unset the variable if
+                                       // its invalid.
+                                       wfDebugLog( 'XMP', __METHOD__ . " <$ns:$tag> failed validation." );
+                                       unset( $this->results['xmp-' . $info['map_group']][$finalName] );
+                               }
+                       } else {
+                               wfDebugLog( 'XMP', __METHOD__ . " Validation function for $finalName ("
+                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+                       }
+               }
+
+               array_shift( $this->curItem );
+               array_shift( $this->mode );
+               $this->ancestorStruct = false;
+               $this->processingArray = false;
+               $this->itemLang = false;
+       }
+
+       /**
+       * Hit a closing element in MODE_LI (either rdf:Seq, or rdf:Bag )
+       * Add information about what type of element this is.
+       *
+       * Note we still have to hit the outer </property>
+       *
+       * For example, when processing:
+       * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+       *   </rdf:Seq> </exif:ISOSpeedRatings>
+       *
+       * This method is called when we hit the </rdf:Seq>.
+       * (For comparison, we call endElementModeSimple when we
+       * hit the </rdf:li>)
+       *
+       * @param $elm String namespace . ' ' . element name
+       */
+       private function endElementModeLi( $elm ) {
+
+               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+               $info = $this->items[$ns][$tag];
+               $finalName = isset( $info['map_name'] )
+                       ? $info['map_name'] : $tag;
+
+               array_shift( $this->mode );
+
+               if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Empty compund element $finalName." );
+                       return;
+               }
+
+               if ( $elm === self::NS_RDF . ' Seq' ) {
+                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ol';
+               } elseif ( $elm === self::NS_RDF . ' Bag' ) {
+                       $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'ul';
+               } elseif ( $elm === self::NS_RDF . ' Alt' ) {
+                       // extra if needed as you could theoretically have a non-language alt.
+                       if ( $info['mode'] === self::MODE_LANG ) {
+                               $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'lang';
+                       }
+
+               } else {
+                       throw new MWException( __METHOD__ . " expected </rdf:seq> or </rdf:bag> but instead got $elm." );
+               }
+       }
+       /**
+       * End element while in MODE_QDESC
+       * mostly when ending an element when we have a simple value
+       * that has qualifiers.
+       *
+       * Qualifiers aren't all that common, and we don't do anything
+       * with them.
+       *
+       * @param $elm String namespace and element
+       */
+       private function endElementModeQDesc( $elm ) {
+
+               if ( $elm === self::NS_RDF . ' value' ) {
+                       list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                       $this->saveValue( $ns, $tag, $this->charContent );
+                       return;
+               } else {
+                       array_shift( $this->mode );
+                       array_shift( $this->curItem );
+               }
+
+
+       }
+       /**
+       * Handler for hitting a closing element.
+       *
+       * generally just calls a helper function depending on what
+       * mode we're in.
+       * 
+       * Ignores the outer wrapping elements that are optional in
+       * xmp and have no meaning.
+       *
+       * @param $parser XMLParser
+       * @param $elm String namespace . ' ' . element name
+       */
+       function endElement( $parser, $elm ) {
+               if ( $elm === ( self::NS_RDF . ' RDF' )
+                       || $elm === 'adobe:ns:meta/ xmpmeta'
+                       || $elm === 'adobe:ns:meta/ xapmeta' )
+               {
+                       // ignore these.
+                       return;
+               }
+
+               if ( $elm === self::NS_RDF . ' type' ) {
+                       // these aren't really supported properly yet.
+                       // However, it appears they almost never used.
+                       wfDebugLog( 'XMP', __METHOD__ . ' encountered <rdf:type>' );
+               }
+
+               if ( strpos( $elm, ' ' ) === false ) {
+                       // This probably shouldn't happen.
+                       // However, there is a bug in an adobe product
+                       // that forgets the namespace on some things.
+                       // (Luckily they are unimportant things).
+                       wfDebugLog( 'XMP', __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." );
+                       return;
+               }
+
+               if ( count( $this->mode[0] ) === 0 ) {
+                       // This should never ever happen and means
+                       // there is a pretty major bug in this class.
+                       throw new MWException( 'Encountered end element with no mode' );
+               }
+
+               if ( count( $this->curItem ) == 0 && $this->mode[0] !== self::MODE_INITIAL ) {
+                       // just to be paranoid. Should always have a curItem, except for initially
+                       // (aka during MODE_INITAL).
+                       throw new MWException( "Hit end element </$elm> but no curItem" );
+               }
+
+               switch( $this->mode[0] ) {
+                       case self::MODE_IGNORE:
+                               $this->endElementModeIgnore( $elm );
+                               break;
+                       case self::MODE_SIMPLE:
+                               $this->endElementModeSimple( $elm );
+                               break;
+                       case self::MODE_STRUCT:
+                       case self::MODE_SEQ:
+                       case self::MODE_BAG:
+                       case self::MODE_LANG:
+                       case self::MODE_BAGSTRUCT:
+                               $this->endElementNested( $elm );
+                               break;
+                       case self::MODE_INITIAL:
+                               if ( $elm === self::NS_RDF . ' Description' ) {
+                                       array_shift( $this->mode );
+                               } else {
+                                       throw new MWException( 'Element ended unexpectedly while in MODE_INITIAL' );
+                               }
+                               break;
+                       case self::MODE_LI:
+                       case self::MODE_LI_LANG:
+                               $this->endElementModeLi( $elm );
+                               break;
+                       case self::MODE_QDESC:
+                               $this->endElementModeQDesc( $elm );
+                               break;
+                       default:
+                               wfDebugLog( 'XMP', __METHOD__ . " no mode (elm = $elm)" );
+                               break;
+               }
+       }
+
+
+       /**
+       * Hit an opening element while in MODE_IGNORE
+       *
+       * XMP is extensible, so ignore any tag we don't understand.
+       *
+       * Mostly ignores, unless we encounter the element that we are ignoring.
+       * in which case we add it to the item stack, so we can ignore things
+       * that are nested, correctly.
+       *
+       * @param $elm String namespace . ' ' . tag name
+       */
+       private function startElementModeIgnore( $elm ) {
+               if ( $elm === $this->curItem[0] ) {
+                       array_unshift( $this->curItem, $elm );
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+               }
+       }
+       /**
+       *  Start element in MODE_BAG (unordered array)
+       * this should always be <rdf:Bag>
+       *
+       * @param $elm String namespace . ' ' . tag
+       * @throws MWException if we have an element that's not <rdf:Bag>
+       */
+       private function startElementModeBag( $elm ) {
+               if ( $elm === self::NS_RDF . ' Bag' ) {
+                       array_unshift( $this->mode, self::MODE_LI );
+               } else {
+                       throw new MWException( "Expected <rdf:Bag> but got $elm." );
+               }
+
+       }
+       /**
+       * Start element in MODE_SEQ (ordered array)
+       * this should always be <rdf:Seq>
+       *
+       * @param $elm String namespace . ' ' . tag
+       * @throws MWException if we have an element that's not <rdf:Seq>
+       */
+       private function startElementModeSeq( $elm ) {
+               if ( $elm === self::NS_RDF . ' Seq' ) {
+                       array_unshift( $this->mode, self::MODE_LI );
+               } else if ( $elm === self::NS_RDF . ' Bag' ) {
+                       # bug 27105
+                       wfDebugLog( 'XMP', __METHOD__ . ' Expected an rdf:Seq, but got an rdf:Bag. Pretending'
+                               . ' it is a Seq, since some buggy software is known to screw this up.' );
+                       array_unshift( $this->mode, self::MODE_LI );
+               } else {
+                       throw new MWException( "Expected <rdf:Seq> but got $elm." );
+               }
+
+       }
+       /**
+       * Start element in MODE_LANG (language alternative)
+       * this should always be <rdf:Alt>
+       *
+       * This tag tends to be used for metadata like describe this
+       * picture, which can be translated into multiple languages.
+       *
+       * XMP supports non-linguistic alternative selections,
+       * which are really only used for thumbnails, which
+       * we don't care about.
+       *
+       * @param $elm String namespace . ' ' . tag
+       * @throws MWException if we have an element that's not <rdf:Alt>
+       */
+       private function startElementModeLang( $elm ) {
+               if ( $elm === self::NS_RDF . ' Alt' ) {
+                       array_unshift( $this->mode, self::MODE_LI_LANG );
+               } else {
+                       throw new MWException( "Expected <rdf:Seq> but got $elm." );
+               }
+
+       }
+       /**
+       * Handle an opening element when in MODE_SIMPLE
+       *
+       * This should not happen often. This is for if a simple element
+       * already opened has a child element. Could happen for a
+       * qualified element.
+       *
+       * For example:
+       * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+       *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+       *   </exif:DigitalZoomRatio>
+       *
+       * This method is called when processing the <rdf:Description> element
+       *
+       * @param $elm String namespace and tag names separated by space.
+       * @param $attribs Array Attributes of the element.
+       */
+       private function startElementModeSimple( $elm, $attribs ) {
+               if ( $elm === self::NS_RDF . ' Description' ) {
+                       // If this value has qualifiers
+                       array_unshift( $this->mode, self::MODE_QDESC );
+                       array_unshift( $this->curItem, $this->curItem[0] );
+
+                       if ( isset( $attribs[self::NS_RDF . ' value'] ) ) {
+                               list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 );
+                               $this->saveValue( $ns, $tag, $attribs[self::NS_RDF . ' value'] );
+                       }
+               } elseif ( $elm === self::NS_RDF . ' value' ) {
+                       // This should not be here.
+                       throw new MWException( __METHOD__ . ' Encountered <rdf:value> where it was unexpected.' );
+
+               } else {
+                       // something else we don't recognize, like a qualifier maybe.
+                       wfDebugLog( 'XMP', __METHOD__ . " Encountered element <$elm> where only expecting character data as value of " . $this->curItem[0] );
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+                       array_unshift( $this->curItem, $elm );
+
+               }
+
+       }
+       /**
+       * Start an element when in MODE_QDESC.
+       * This generally happens when a simple element has an inner
+       * rdf:Description to hold qualifier elements.
+       *
+       * For example in:
+       * <exif:DigitalZoomRatio><rdf:Description><rdf:value>0/10</rdf:value>
+       *   <foo:someQualifier>Bar</foo:someQualifier> </rdf:Description>
+       *   </exif:DigitalZoomRatio>
+       * Called when processing the <rdf:value> or <foo:someQualifier>.
+       *
+       * @param $elm String namespace and tag name separated by a space.
+       *
+       */
+       private function startElementModeQDesc( $elm ) {
+               if ( $elm === self::NS_RDF . ' value' ) {
+                       return; // do nothing
+               } else {
+                       // otherwise its a qualifier, which we ignore
+                       array_unshift( $this->mode, self::MODE_IGNORE );
+                       array_unshift( $this->curItem, $elm );
+               }
+       }
+       /**
+       * Starting an element when in MODE_INITIAL
+       * This usually happens when we hit an element inside
+       * the outer rdf:Description
+       *
+       * This is generally where most properties start.
+       *
+       * @param $ns String Namespace
+       * @param $tag String tag name (without namespace prefix)
+       * @param $attribs Array array of attributes
+       */
+       private function startElementModeInitial( $ns, $tag, $attribs ) {
+               if ( $ns !== self::NS_RDF ) {
+
+                       if ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( isset( $this->items[$ns][$tag]['structPart'] ) ) {
+                                       // If this element is supposed to appear only as
+                                       // a child of a structure, but appears here (not as
+                                       // a child of a struct), then something weird is
+                                       // happening, so ignore this element and its children.
+
+                                       wfDebugLog( 'XMP', "Encountered <$ns:$tag> outside"
+                                               . " of its expected parent. Ignoring." );
+
+                                       array_unshift( $this->mode, self::MODE_IGNORE );
+                                       array_unshift( $this->curItem, $ns . ' ' . $tag );
+                                       return;
+                               }
+                               $mode = $this->items[$ns][$tag]['mode'];
+                               array_unshift( $this->mode, $mode );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+                               if ( $mode === self::MODE_STRUCT ) {
+                                       $this->ancestorStruct = isset( $this->items[$ns][$tag]['map_name'] )
+                                               ? $this->items[$ns][$tag]['map_name'] : $tag;
+                               }
+                               if ( $this->charContent !== false ) {
+                                       // Something weird.
+                                       // Should not happen in valid XMP.
+                                       throw new MWException( 'tag nested in non-whitespace characters.' );
+                               }
+                       } else {
+                               // This element is not on our list of allowed elements so ignore.
+                               wfDebugLog( 'XMP', __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+                               array_unshift( $this->mode, self::MODE_IGNORE );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+                               return;
+                       }
+
+               }
+               // process attributes
+               $this->doAttribs( $attribs );
+       }
+       /**
+       * Hit an opening element when in a Struct (MODE_STRUCT)
+       * This is generally for fields of a compound property.
+       *
+       * Example of a struct (abbreviated; flash has more properties):
+       *
+       * <exif:Flash> <rdf:Description> <exif:Fired>True</exif:Fired>
+       *  <exif:Mode>1</exif:Mode></rdf:Description></exif:Flash>
+       *
+       * or:
+       * 
+       * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired>
+       *  <exif:Mode>1</exif:Mode></exif:Flash> 
+       *
+       * @param $ns String namespace
+       * @param $tag String tag name (no ns)
+       * @param $attribs Array array of attribs w/ values.
+       */
+       private function startElementModeStruct( $ns, $tag, $attribs ) {
+               if ( $ns !== self::NS_RDF ) {
+
+                       if ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( isset( $this->items[$ns][$this->ancestorStruct]['children'] )
+                                       && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] ) )
+                               {
+                                       // This assumes that we don't have inter-namespace nesting
+                                       // which we don't in all the properties we're interested in.
+                                       throw new MWException( " <$tag> appeared nested in <" . $this->ancestorStruct
+                                               . "> where it is not allowed." );
+                               }
+                               array_unshift( $this->mode, $this->items[$ns][$tag]['mode'] );
+                               array_unshift( $this->curItem, $ns . ' ' . $tag );
+                               if ( $this->charContent !== false ) {
+                                       // Something weird.
+                                       // Should not happen in valid XMP.
+                                       throw new MWException( "tag <$tag> nested in non-whitespace characters (" . $this->charContent . ")." );
+                               }
+                       } else {
+                               array_unshift( $this->mode, self::MODE_IGNORE );
+                               array_unshift( $this->curItem, $elm );
+                               return;
+                       }
+
+               }
+
+               if ( $ns === self::NS_RDF && $tag === 'Description' ) {
+                       $this->doAttribs( $attribs );
+                       array_unshift( $this->mode, self::MODE_STRUCT );
+                       array_unshift( $this->curItem, $this->curItem[0] );
+               }
+       }
+       /**
+       * opening element in MODE_LI
+       * process elements of arrays.
+       *
+       * Example:
+       * <exif:ISOSpeedRatings> <rdf:Seq> <rdf:li>64</rdf:li>
+       *   </rdf:Seq> </exif:ISOSpeedRatings>
+       * This method is called when we hit the <rdf:li> element.
+       *
+       * @param $elm String: namespace . ' ' . tagname
+       * @param $attribs Array: Attributes. (needed for BAGSTRUCTS) 
+       * @throws MWException if gets a tag other than <rdf:li>
+       */
+       private function startElementModeLi( $elm, $attribs ) {
+               if ( ( $elm ) !== self::NS_RDF . ' li' ) {
+                       throw new MWException( "<rdf:li> expected but got $elm." );
+               }
+
+               if ( !isset( $this->mode[1] ) ) {
+                       // This should never ever ever happen. Checking for it
+                       // to be paranoid.
+                       throw new MWException( 'In mode Li, but no 2xPrevious mode!' );
+               }
+
+               if ( $this->mode[1] === self::MODE_BAGSTRUCT ) {
+                       // This list item contains a compound (STRUCT) value.
+                       array_unshift( $this->mode, self::MODE_STRUCT );
+                       array_unshift( $this->curItem, $elm );
+                       $this->processingArray = true;
+
+                       if ( !isset( $this->curItem[1] ) ) {
+                               // be paranoid.
+                               throw new MWException( 'Can not find parent of BAGSTRUCT.' );
+                       }
+                       list( $curNS, $curTag ) = explode( ' ', $this->curItem[1] );
+                       $this->ancestorStruct = isset( $this->items[$curNS][$curTag]['map_name'] )
+                               ? $this->items[$curNS][$curTag]['map_name'] : $curTag;
+
+                       $this->doAttribs( $attribs );
+
+               } else {
+                       // Normal BAG or SEQ containing simple values.
+                       array_unshift( $this->mode, self::MODE_SIMPLE );
+                       // need to add curItem[0] on again since one is for the specific item
+                       // and one is for the entire group.
+                       array_unshift( $this->curItem, $this->curItem[0] );
+                       $this->processingArray = true;
+               }
+
+       }
+       /**
+       * Opening element in MODE_LI_LANG.
+       * process elements of language alternatives
+       *
+       * Example:
+       * <dc:title> <rdf:Alt> <rdf:li xml:lang="x-default">My house
+       *  </rdf:li> </rdf:Alt> </dc:title>
+       *
+       * This method is called when we hit the <rdf:li> element.
+       *
+       * @param $elm String namespace . ' ' . tag
+       * @param $attribs array array of elements (most importantly xml:lang)
+       * @throws MWException if gets a tag other than <rdf:li> or if no xml:lang
+       */
+       private function startElementModeLiLang( $elm, $attribs ) {
+               if ( $elm !== self::NS_RDF . ' li' ) {
+                       throw new MWException( __METHOD__ . " <rdf:li> expected but got $elm." );
+               }
+               if ( !isset( $attribs[ self::NS_XML . ' lang'] )
+                       || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[ self::NS_XML . ' lang' ] ) )
+               {
+                       throw new MWException( __METHOD__
+                               . " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" );
+               }
+
+               // Lang is case-insensitive.
+               $this->itemLang = strtolower( $attribs[ self::NS_XML . ' lang' ] );
+
+               // need to add curItem[0] on again since one is for the specific item
+               // and one is for the entire group.
+               array_unshift( $this->curItem, $this->curItem[0] );
+               array_unshift( $this->mode, self::MODE_SIMPLE );
+               $this->processingArray = true;
+       }
+
+       /**
+       * Hits an opening element.
+       * Generally just calls a helper based on what MODE we're in.
+       * Also does some initial set up for the wrapper element
+       *
+       * @param $parser XMLParser
+       * @param $elm String namespace <space> element
+       * @param $attribs Array attribute name => value
+       */
+       function startElement( $parser, $elm, $attribs ) {
+
+               if ( $elm === self::NS_RDF . ' RDF'
+                       || $elm === 'adobe:ns:meta/ xmpmeta' 
+                       || $elm === 'adobe:ns:meta/ xapmeta')
+               {
+                       /* ignore. */
+                       return;
+               } elseif ( $elm === self::NS_RDF . ' Description' ) {
+                       if ( count( $this->mode ) === 0 ) {
+                               // outer rdf:desc
+                               array_unshift( $this->mode, self::MODE_INITIAL );
+                       }
+               } elseif ( $elm === self::NS_RDF . ' type' ) {
+                       // This doesn't support rdf:type properly.
+                       // In practise I have yet to see a file that
+                       // uses this element, however it is mentioned
+                       // on page 25 of part 1 of the xmp standard.
+                       //
+                       // also it seems as if exiv2 and exiftool do not support
+                       // this either (That or I misunderstand the standard)
+                       wfDebugLog( 'XMP', __METHOD__ . ' Encountered <rdf:type> which isn\'t currently supported' );
+               }
+
+               if ( strpos( $elm, ' ' ) === false ) {
+                       // This probably shouldn't happen.
+                       wfDebugLog( 'XMP', __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." );
+                       return;
+               }
+
+               list( $ns, $tag ) = explode( ' ', $elm, 2 );
+
+               if ( count( $this->mode ) === 0 ) {
+                       // This should not happen.
+                       throw new MWException('Error extracting XMP, '
+                               . "encountered <$elm> with no mode" );
+               }
+
+               switch( $this->mode[0] ) {
+                       case self::MODE_IGNORE:
+                               $this->startElementModeIgnore( $elm );
+                               break;
+                       case self::MODE_SIMPLE:
+                               $this->startElementModeSimple( $elm, $attribs );
+                               break;
+                       case self::MODE_INITIAL:
+                               $this->startElementModeInitial( $ns, $tag, $attribs );
+                               break;
+                       case self::MODE_STRUCT:
+                               $this->startElementModeStruct( $ns, $tag, $attribs );
+                               break;
+                       case self::MODE_BAG:
+                       case self::MODE_BAGSTRUCT:
+                               $this->startElementModeBag( $elm );
+                               break;
+                       case self::MODE_SEQ:
+                               $this->startElementModeSeq( $elm );
+                               break;
+                       case self::MODE_LANG:
+                               $this->startElementModeLang( $elm );
+                               break;
+                       case self::MODE_LI_LANG:
+                               $this->startElementModeLiLang( $elm, $attribs );
+                               break;
+                       case self::MODE_LI:
+                               $this->startElementModeLi( $elm, $attribs );
+                               break;
+                       case self::MODE_QDESC:
+                               $this->startElementModeQDesc( $elm );
+                               break;
+                       default:
+                               throw new MWException( 'StartElement in unknown mode: ' . $this->mode[0] );
+                               break;
+               }
+
+
+
+       }
+       /**
+       * Process attributes.
+       * Simple values can be stored as either a tag or attribute
+       *
+       * Often the initial <rdf:Description> tag just has all the simple
+       * properties as attributes.
+       *
+       * Example:
+       * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10">
+       *
+       * @param $attribs Array attribute=>value array.
+       */
+       private function doAttribs( $attribs ) {
+               
+               // first check for rdf:parseType attribute, as that can change
+               // how the attributes are interperted.
+
+               if ( isset( $attribs[self::NS_RDF . ' parseType'] )
+                       && $attribs[self::NS_RDF . ' parseType'] === 'Resource'
+                       && $this->mode[0] === self::MODE_SIMPLE )
+               {
+                       // this is equivalent to having an inner rdf:Description
+                       $this->mode[0] = self::MODE_QDESC;
+               }
+               foreach ( $attribs as $name => $val ) {
+
+
+                       if ( strpos( $name, ' ' ) === false ) {
+                               // This shouldn't happen, but so far some old software forgets namespace
+                               // on rdf:about.
+                               wfDebugLog( 'XMP', __METHOD__ . ' Encountered non-namespaced attribute: '
+                                       . " $name=\"$val\". Skipping. " );
+                               continue;
+                       }
+                       list( $ns, $tag ) = explode( ' ', $name, 2 );
+                       if ( $ns === self::NS_RDF ) {
+                               if ( $tag === 'value' || $tag === 'resource' ) {
+                                       // resource is for url.
+                                       // value attribute is a weird way of just putting the contents.
+                                       $this->char( $this->xmlParser, $val );
+                               }
+                       } elseif ( isset( $this->items[$ns][$tag] ) ) {
+                               if ( $this->mode[0] === self::MODE_SIMPLE ) {
+                                       throw new MWException( __METHOD__
+                                               . " $ns:$tag found as attribute where not allowed" );
+                               }
+                               $this->saveValue( $ns, $tag, $val );
+                       } else {
+                               wfDebugLog( 'XMP', __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." );
+                       }
+               }
+       }
+       /**
+       * Given an extracted value, save it to results array
+       *
+       * note also uses $this->ancestorStruct and
+       * $this->processingArray to determine what name to
+       * save the value under. (in addition to $tag).
+       *
+       * @param $ns String namespace of tag this is for
+       * @param $tag String tag name
+       * @param $val String value to save
+       */
+       private function saveValue( $ns, $tag, $val ) {
+
+               $info =& $this->items[$ns][$tag];
+               $finalName = isset( $info['map_name'] )
+                       ? $info['map_name'] : $tag;
+               if ( isset( $info['validate'] ) ) {
+                       $validate = is_array( $info['validate'] ) ? $info['validate']
+                               : array( 'XMPValidate', $info['validate'] );
+
+                       if ( is_callable( $validate ) ) {
+                               call_user_func_array( $validate, array( $info, &$val, true ) );
+                               // the reasoning behind using &$val instead of using the return value
+                               // is to be consistent between here and validating structures.
+                               if ( is_null( $val ) ) {
+                                       wfDebugLog( 'XMP', __METHOD__ . " <$ns:$tag> failed validation." );
+                                       return;
+                               }
+                       } else {
+                               wfDebugLog( 'XMP', __METHOD__ . " Validation function for $finalName ("
+                                       . $validate[0] . '::' . $validate[1] . '()) is not callable.' );
+                       }
+               }
+
+               if ( $this->ancestorStruct && $this->processingArray ) {
+                       // Aka both an array and a struct. ( self::MODE_BAGSTRUCT )
+                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][][$finalName] = $val;
+               } elseif ( $this->ancestorStruct ) {
+                       $this->results['xmp-' . $info['map_group']][$this->ancestorStruct][$finalName] = $val;
+               } elseif ( $this->processingArray ) {
+                       if ( $this->itemLang === false ) {
+                               // normal array
+                               $this->results['xmp-' . $info['map_group']][$finalName][] = $val;
+                       } else {
+                               // lang array.
+                               $this->results['xmp-' . $info['map_group']][$finalName][$this->itemLang] = $val;
+                       }
+               } else {
+                       $this->results['xmp-' . $info['map_group']][$finalName] = $val;
+               }
+       }
+
+
+}
diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php
new file mode 100644 (file)
index 0000000..4dbc1cb
--- /dev/null
@@ -0,0 +1,1136 @@
+<?php
+/**
+* This class is just a container for a big array
+* used by XMPReader to determine which XMP items to
+* extract.
+*/
+class XMPInfo {
+
+       /** get the items array
+        * @return Array XMP item configuration array.
+       */
+       public static function getItems ( ) {
+               if( !self::$ranHooks ) {
+                       // This is for if someone makes a custom metadata extension.
+                       // For example, a medical wiki might want to decode DICOM xmp properties.
+                       wfRunHooks('XMPGetInfo', Array(&self::$items));
+                       self::$ranHooks = true; // Only want to do this once.
+               }
+               return self::$items;
+       }
+
+       static private $ranHooks = false;
+
+       /**
+       * XMPInfo::$items keeps a list of all the items
+       * we are interested to extract, as well as
+       * information about the item like what type
+       * it is.
+       *
+       * Format is an array of namespaces,
+       * each containing an array of tags
+       * each tag is an array of information about the
+       * tag, including:
+       *       * map_group - what group (used for precedence during conflicts)
+       *       * mode - What type of item (self::MODE_SIMPLE usually, see above for all values)
+       *       * validate - method to validate input. Could also post-process the input. A string value is assumed to be a static method of XMPValidate. Can also take a array( 'className', 'methodName' ).
+       *       * choices  - array of potential values (format of 'value' => true ). Only used with validateClosed
+       *       * rangeLow and rangeHigh - alternative to choices for numeric ranges. Again for validateClosed only.
+       *       * children - for MODE_STRUCT items, allowed children.
+       *       * structPart - Indicates that this element can only appear as a member of a structure.
+       *
+       * currently this just has a bunch of exif values as this class is only half-done
+       */
+
+       static private $items = array(
+               'http://ns.adobe.com/exif/1.0/' => array(
+                       'ApertureValue' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'BrightnessValue' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'CompressedBitsPerPixel' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'DigitalZoomRatio' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'ExposureBiasValue' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'ExposureIndex' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'ExposureTime' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'FlashEnergy' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational',
+                       ),
+                       'FNumber' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'FocalLength' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'FocalPlaneXResolution' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'FocalPlaneYResolution' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSAltitude' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational',
+                       ),
+                       'GPSDestBearing' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSDestDistance' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSDOP' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSImgDirection' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSSpeed' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'GPSTrack' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'MaxApertureValue'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'ShutterSpeedValue' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       'SubjectDistance'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational'
+                       ),
+                       /* Flash */
+                       'Flash'             => array(
+                               'mode'      => XMPReader::MODE_STRUCT,
+                               'children'  => array(
+                                       'Fired'      => true,
+                                       'Function'   => true,
+                                       'Mode'       => true,
+                                       'RedEyeMode' => true,
+                                       'Return'     => true,
+                               ),
+                               'validate'  => 'validateFlash',
+                               'map_group' => 'exif',
+                       ),
+                       'Fired'             => array(
+                               'map_group' => 'exif',
+                               'validate'  => 'validateBoolean',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'Function'          => array(
+                               'map_group' => 'exif',
+                               'validate'  => 'validateBoolean',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'Mode'              => array(
+                               'map_group' => 'exif',
+                               'validate'  => 'validateClosed',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'choices'   => array( '0' => true, '1' => true,
+                                               '2' => true, '3' => true ),
+                               'structPart'=> true,
+                       ),
+                       'Return'            => array(
+                               'map_group' => 'exif',
+                               'validate'  => 'validateClosed',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'choices'   => array( '0' => true,
+                                               '2' => true, '3' => true ),
+                               'structPart'=> true,
+                       ),
+                       'RedEyeMode'        => array(
+                               'map_group' => 'exif',
+                               'validate'  => 'validateBoolean',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       /* End Flash */
+                       'ISOSpeedRatings'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateInteger'
+                       ),
+                       /* end rational things */
+                       'ColorSpace'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '65535' => true ),
+                       ),
+                       'ComponentsConfiguration'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '2' => true, '3' => true, '4' => true,
+                                               '5' => true, '6' => true )
+                       ),
+                       'Contrast'          => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '0' => true, '1' => true, '2' => true )
+                       ),
+                       'CustomRendered'    => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '0' => true, '1' => true )
+                       ),
+                       'DateTimeOriginal'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateDate',
+                       ),
+                       'DateTimeDigitized' => array(  /* xmp:CreateDate */
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateDate',
+                       ),
+                       /* todo: there might be interesting information in
+                        * exif:DeviceSettingDescription, but need to find an
+                        * example
+                        */
+                       'ExifVersion'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'ExposureMode'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 2,
+                       ),
+                       'ExposureProgram'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 8,
+                       ),
+                       'FileSource'        => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '3' => true )
+                       ),
+                       'FlashpixVersion'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'FocalLengthIn35mmFilm' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'FocalPlaneResolutionUnit' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '2' => true, '3' => true ),
+                       ),
+                       'GainControl'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 4,
+                       ),
+                       /* this value is post-processed out later */
+                       'GPSAltitudeRef'    => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '0' => true, '1' => true ),
+                       ),
+                       'GPSAreaInformation' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'GPSDestBearingRef' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( 'T' => true, 'M' => true ),
+                       ),
+                       'GPSDestDistanceRef' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( 'K' => true, 'M' => true,
+                                               'N' => true ),
+                       ),
+                       'GPSDestLatitude'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateGPS',
+                       ),
+                       'GPSDestLongitude'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateGPS',
+                       ),
+                       'GPSDifferential'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '0' => true, '1' => true ),
+                       ),
+                       'GPSImgDirectionRef' => array(
+                               'map_group'  => 'exif',
+                               'mode'       => XMPReader::MODE_SIMPLE,
+                               'validate'   => 'validateClosed',
+                               'choices'    => array( 'T' => true, 'M' => true ),
+                       ),
+                       'GPSLatitude'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateGPS',
+                       ),
+                       'GPSLongitude'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateGPS',
+                       ),
+                       'GPSMapDatum'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'GPSMeasureMode'    => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '2' => true, '3' => true )
+                       ),
+                       'GPSProcessingMethod' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'GPSSatellites'     => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'GPSSpeedRef'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( 'K' => true, 'M' => true,
+                                               'N' => true ),
+                       ),
+                       'GPSStatus'         => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( 'A' => true, 'V' => true )
+                       ),
+                       'GPSTimeStamp'      => array(
+                               'map_group' => 'exif',
+                               // Note: in exif, GPSDateStamp does not include
+                               // the time, where here it does.
+                               'map_name'  => 'GPSDateStamp',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateDate',
+                       ),
+                       'GPSTrackRef'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( 'T' => true, 'M' => true )
+                       ),
+                       'GPSVersionID'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'ImageUniqueID'     => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'LightSource'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               /* can't use a range, as it skips... */
+                               'choices'   =>  array( '0' => true, '1' => true,
+                                       '2' => true, '3' => true, '4' => true,
+                                       '9' => true, '10' => true, '11' => true,
+                                       '12' => true, '13' => true,
+                                       '14' => true, '15' => true,
+                                       '17' => true, '18' => true,
+                                       '19' => true, '20' => true,
+                                       '21' => true, '22' => true,
+                                       '23' => true, '24' => true,
+                                       '255' => true,
+                               ),
+                       ),
+                       'MeteringMode'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 6,
+                               'choices'   => array( '255' => true ),
+                       ),
+                       /* Pixel(X|Y)Dimension are rather useless, but for
+                        * completeness since we do it with exif.
+                        */
+                       'PixelXDimension'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'PixelYDimension'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'Saturation'        => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 2,
+                       ),
+                       'SceneCaptureType'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 3,
+                       ),
+                       'SceneType'         => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true ),
+                       ),
+                       // Note, 6 is not valid SensingMethod.
+                       'SensingMethod'     => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 1,
+                               'rangeHigh' => 5,
+                               'choices'   => array( '7' => true, 8 => true ),
+                       ),
+                       'Sharpness'         => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 2,
+                       ),
+                       'SpectralSensitivity' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       // This tag should perhaps be displayed to user better.
+                       'SubjectArea'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'SubjectDistanceRange' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'rangeLow'  => 0,
+                               'rangeHigh' => 3,
+                       ),
+                       'SubjectLocation'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'UserComment'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_LANG,
+                       ),
+                       'WhiteBalance'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '0' => true, '1' => true )
+                       ),
+               ),
+               'http://ns.adobe.com/tiff/1.0/' => array(
+                       'Artist'            => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'BitsPerSample'     => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'Compression'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '6' => true ),
+                       ),
+                       /* this prop should not be used in XMP. dc:rights is the correct prop */
+                       'Copyright'         => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_LANG,
+                       ),
+                       'DateTime'          => array(  /* proper prop is xmp:ModifyDate */
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateDate',
+                       ),
+                       'ImageDescription'  => array(  /* proper one is dc:description */
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_LANG,
+                       ),
+                       'ImageLength'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'ImageWidth'        => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'Make'              => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Model'             => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Orientation'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true,
+                                               '6' => true, '7' => true, '8' => true ),
+                       ),
+                       'PhotometricInterpretation' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '2' => true, '6' => true ),
+                       ),
+                       'PlanerConfiguration' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '2' => true ),
+                       ),
+                       'PrimaryChromaticities' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateRational',
+                       ),
+                       'ReferenceBlackWhite' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateRational',
+                       ),
+                       'ResolutionUnit'    => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '2' => true, '3' => true ),
+                       ),
+                       'SamplesPerPixel'   => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                       ),
+                       'Software'          => array(  /* see xmp:CreatorTool */
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       /* ignore TransferFunction */
+                       'WhitePoint'        => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateRational',
+                       ),
+                       'XResolution'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational',
+                       ),
+                       'YResolution'       => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRational',
+                       ),
+                       'YCbCrCoefficients' => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateRational',
+                       ),
+                       'YCbCrPositioning'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '2' => true ),
+                       ),
+                       'YCbCrSubSampling'  => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateClosed',
+                               'choices'   => array( '1' => true, '2' => true ),
+                       ),
+               ),
+               'http://ns.adobe.com/exif/1.0/aux/' => array(
+                       'Lens'              => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'SerialNumber'      => array(
+                               'map_group' => 'exif',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'OwnerName'         => array(
+                               'map_group' => 'exif',
+                               'map_name'  => 'CameraOwnerName',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+               ),
+               'http://purl.org/dc/elements/1.1/' => array(
+                       'title'             => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'ObjectName',
+                               'mode'      => XMPReader::MODE_LANG
+                       ),
+                       'description'       => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'ImageDescription',
+                               'mode'      => XMPReader::MODE_LANG
+                       ),
+                       'contributor'       => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-contributor',
+                               'mode'      => XMPReader::MODE_BAG
+                       ),
+                       'coverage'          => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-coverage',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'creator'           => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'Artist', //map with exif Artist, iptc byline (2:80)
+                               'mode'      => XMPReader::MODE_SEQ,
+                       ),
+                       'date'              => array(
+                               'map_group' => 'general',
+                               // Note, not mapped with other date properties, as this type of date is
+                               // non-specific: "A point or period of time associated with an event in
+                               //  the lifecycle of the resource"
+                               'map_name'  => 'dc-date',
+                               'mode'      => XMPReader::MODE_SEQ,
+                               'validate'  => 'validateDate',
+                       ),
+                       /* Do not extract dc:format, as we've got better ways to determine mimetype */
+                       'identifier'        => array(
+                               'map_group' => 'deprecated',
+                               'map_name'  => 'Identifier',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'language'          => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'LanguageCode', /* mapped with iptc 2:135 */
+                               'mode'      => XMPReader::MODE_BAG,
+                               'validate'  => 'validateLangCode',
+                       ),
+                       'publisher'         => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-publisher',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       // for related images/resources
+                       'relation'          => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-relation',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       'rights'            => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'Copyright',
+                               'mode'      => XMPReader::MODE_LANG,
+                       ),
+                       // Note: source is not mapped with iptc source, since iptc
+                       // source describes the source of the image in terms of a person
+                       // who provided the image, where this is to describe an image that the
+                       // current one is based on.
+                       'source'            => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-source',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'subject'           => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'Keywords', /* maps to iptc 2:25 */
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       'type'              => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'dc-type',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+               ),
+               'http://ns.adobe.com/xap/1.0/' => array(
+                       'CreateDate' => array(
+                               'map_group' => 'general',
+                               'map_name' => 'DateTimeDigitized',
+                               'mode'     => XMPReader::MODE_SIMPLE,
+                               'validate' => 'validateDate',
+                       ),
+                       'CreatorTool' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'Software',
+                               'mode'      => XMPReader::MODE_SIMPLE
+                       ),
+                       'Identifier' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       'Label' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'ModifyDate' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'DateTime',
+                               'validate'  => 'validateDate',
+                       ),
+                       'MetadataDate' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               // map_name to be consistent with other date names.
+                               'map_name'  => 'DateTimeMetadata',
+                               'validate'  => 'validateDate',
+                       ),
+                       'Nickname' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Rating' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateRating',
+                       ),
+               ),
+               'http://ns.adobe.com/xap/1.0/rights/' => array(
+                       'Certificate' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'RightsCertificate',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Marked' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'Copyrighted',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateBoolean',
+                       ),
+                       'Owner' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'CopyrightOwner',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       // this seems similar to dc:rights.
+                       'UsageTerms' => array(
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_LANG,
+                       ),
+                       'WebStatement' => array(
+                               'map_group' => 'general',
+                               'mode' => XMPReader::MODE_SIMPLE,
+                       ),
+               ),
+               // XMP media management.
+               'http://ns.adobe.com/xap/1.0/mm/' => array(
+                       // if we extract the exif UniqueImageID, might
+                       // as well do this too.
+                       'OriginalDocumentID' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       // It might also be useful to do xmpMM:LastURL
+                       // and xmpMM:DerivedFrom as you can potentially,
+                       // get the url of this document/source for this
+                       // document. However whats more likely is you'd
+                       // get a file:// url for the path of the doc,
+                       // which is somewhat of a privacy issue.
+               ),
+               'http://creativecommons.org/ns#' => array(
+                       'license' => array(
+                               'map_name'  => 'LicenseUrl',
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'morePermissions' => array(
+                               'map_name'  => 'MorePermissionsUrl',
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'attributionURL' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'AttributionUrl',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'attributionName' => array(
+                               'map_group' => 'general',
+                               'map_name'  => 'PreferredAttributionName',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+               ),
+               //Note, this property affects how jpeg metadata is extracted.
+               'http://ns.adobe.com/xmp/note/' => array(
+                       'HasExtendedXMP' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+               ),
+               /* Note, in iptc schemas, the legacy properties are denoted
+                * as deprecated, since other properties should used instead,
+                * and properties marked as deprecated in the standard are
+                * are marked as general here as they don't have replacements
+                */
+               'http://ns.adobe.com/photoshop/1.0/' => array(
+                       'City' => array(
+                               'map_group' => 'deprecated',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'CityDest',
+                       ),
+                       'Country' => array(
+                               'map_group' => 'deprecated',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'CountryDest',
+                       ),
+                       'State' => array(
+                               'map_group' => 'deprecated',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'ProvinceOrStateDest',
+                       ),
+                       'DateCreated' => array(
+                               'map_group' => 'deprecated',
+                               // marking as deprecated as the xmp prop preferred
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'DateTimeOriginal',
+                               'validate'  => 'validateDate',
+                               // note this prop is an XMP, not IPTC date
+                       ),
+                       'CaptionWriter' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'Writer',
+                       ),
+                       'Instructions' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'SpecialInstructions',
+                       ),
+                       'TransmissionReference' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'OriginalTransmissionRef',
+                       ),
+                       'AuthorsPosition' => array(
+                               /* This corresponds with 2:85
+                                * By-line Title, which needs to be
+                                * handled weirdly to correspond
+                                * with iptc/exif. */
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE
+                       ),
+                       'Credit' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Source' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Urgency' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'Category' => array(
+                               // Note, this prop is deprecated, but in general
+                               // group since it doesn't have a replacement.
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'iimCategory',
+                       ),
+                       'SupplementalCategories' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                               'map_name'  => 'iimSupplementalCategory',
+                       ),
+                       'Headline' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE
+                       ),
+               ),
+               'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' => array(
+                       'CountryCode' => array(
+                               'map_group' => 'deprecated',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'CountryCodeDest',
+                       ),
+                       'IntellectualGenre' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       // Note, this is a six digit code.
+                       // See: http://cv.iptc.org/newscodes/scene/
+                       // Since these aren't really all that common,
+                       // we just show the number.
+                       'Scene' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                               'validate'  => 'validateInteger',
+                               'map_name'  => 'SceneCode',
+                       ),
+                       /* Note: SubjectCode should be an 8 ascii digits.
+                        * it is not really an integer (has leading 0's,
+                        * cannot have a +/- sign), but validateInteger
+                        * will let it through.
+                        */
+                       'SubjectCode' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                               'map_name'  => 'SubjectNewsCode',
+                               'validate'  => 'validateInteger'
+                       ),
+                       'Location' => array(
+                               'map_group' => 'deprecated',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'map_name'  => 'SublocationDest',
+                       ),
+                       'CreatorContactInfo' => array(
+                               /* Note this maps to 2:118 in iim
+                                * (Contact) field. However those field
+                                * types are slightly different - 2:118
+                                * is free form text field, where this
+                                * is more structured.
+                                */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_STRUCT,
+                               'map_name'  => 'Contact',
+                               'children'  => array(
+                                       'CiAdrExtadr' => true,
+                                       'CiAdrCity'   => true,
+                                       'CiAdrCtry'   => true,
+                                       'CiEmailWork' => true,
+                                       'CiTelWork'   => true,
+                                       'CiAdrPcode'  => true,
+                                       'CiAdrRegion' => true,
+                                       'CiUrlWork'   => true,
+                               ),
+                       ),
+                       'CiAdrExtadr' => array( /* address */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiAdrCity' => array( /* city */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiAdrCtry' => array( /* country */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiEmailWork' => array( /* email (possibly separated by ',') */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiTelWork' => array( /* telephone */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiAdrPcode' => array( /* postal code */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiAdrRegion' => array( /* province/state */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CiUrlWork' => array( /* url. Multiple may be separated by comma. */
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       /* End contact info struct properties */
+               ),
+               'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' => array(
+                       'Event' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                       ),
+                       'OrganisationInImageName' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                               'map_name'  => 'OrganisationInImage'
+                       ),
+                       'PersonInImage' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_BAG,
+                       ),
+                       'MaxAvailHeight' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                               'map_name'  => 'OriginalImageHeight',
+                       ),
+                       'MaxAvailWidth' => array(
+                               'map_group' => 'general',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'validate'  => 'validateInteger',
+                               'map_name'  => 'OriginalImageWidth',
+                       ),
+                       // LocationShown and LocationCreated are handled
+                       // specially because they are hierarchical, but we
+                       // also want to merge with the old non-hierarchical.
+                       'LocationShown' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_BAGSTRUCT,
+                               'children'  => array(
+                                       'WorldRegion' => true,
+                                       'CountryCode' => true, /* iso code */
+                                       'CountryName' => true,
+                                       'ProvinceState' => true,
+                                       'City' => true,
+                                       'Sublocation' => true,
+                               ),
+                       ),
+                       'LocationCreated' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_BAGSTRUCT,
+                               'children'  => array(
+                                       'WorldRegion' => true,
+                                       'CountryCode' => true, /* iso code */
+                                       'CountryName' => true,
+                                       'ProvinceState' => true,
+                                       'City' => true,
+                                       'Sublocation' => true,
+                               ),
+                       ),
+                       'WorldRegion' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CountryCode' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'CountryName' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                               'map_name'  => 'Country',
+                       ),
+                       'ProvinceState' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                               'map_name'  => 'ProvinceOrState',
+                       ),
+                       'City' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+                       'Sublocation' => array(
+                               'map_group' => 'special',
+                               'mode'      => XMPReader::MODE_SIMPLE,
+                               'structPart'=> true,
+                       ),
+
+                       /* Other props that might be interesting but
+                        * Not currently extracted:
+                        * ArtworkOrObject, (info about objects in picture)
+                        * DigitalSourceType
+                        * RegistryId
+                        */
+               ),
+
+               /* Plus props we might want to consider:
+                * (Note: some of these have unclear/incomplete definitions
+                * from the iptc4xmp standard).
+                * ImageSupplier (kind of like iptc source field)
+                * ImageSupplierId (id code for image from supplier)
+                * CopyrightOwner
+                * ImageCreator
+                * Licensor
+                * Various model release fields
+                * Property release fields.
+                */
+       );
+}
diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php
new file mode 100644 (file)
index 0000000..382a514
--- /dev/null
@@ -0,0 +1,317 @@
+<?php
+/**
+* This contains some static methods for
+* validating XMP properties. See XMPInfo and XMPReader classes.
+*
+* Each of these functions take the same parameters
+* * an info array which is a subset of the XMPInfo::items array
+* * A value (passed as reference) to validate. This can be either a
+*      simple value or an array
+* * A boolean to determine if this is validating a simple or complex values
+*
+* It should be noted that when an array is being validated, typically the validation
+* function is called once for each value, and then once at the end for the entire array.
+*
+* These validation functions can also be used to modify the data. See the gps and flash one's
+* for example.
+*
+* @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf starting at pg 28
+* @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf starting at pg 11
+*/
+class XMPValidate {
+       /**
+       * function to validate boolean properties ( True or False )
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateBoolean( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( $val !== 'True' && $val !== 'False' ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected True or False but got $val" );
+                       $val = null;
+               }
+
+       }
+       /**
+       * function to validate rational properties ( 12/10 )
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateRational( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^(?:-?\d+)\/(?:\d+[1-9]|[1-9]\d*)$/D', $val ) ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected rational but got $val" );
+                       $val = null;
+               }
+
+       }
+       /**
+       * function to validate rating properties -1, 0-5
+       *
+       * if its outside of range put it into range.
+       *
+       * @see MWG spec
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateRating( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-+]?\d*(?:\.?\d*)$/D', $val )
+                       || !is_numeric($val)
+                ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected rating but got $val" );
+                       $val = null;
+                       return;
+               } else {
+                       $nVal = (float) $val;
+                       if ( $nVal < 0 ) {
+                               // We do < 0 here instead of < -1 here, since
+                               // the values between 0 and -1 are also illegal
+                               // as -1 is meant as a special reject rating.
+                               wfDebugLog( 'XMP', __METHOD__ . " Rating too low, setting to -1 (Rejected)");
+                               $val = '-1';
+                               return;
+                       }
+                       if ( $nVal > 5 ) {
+                               wfDebugLog( 'XMP', __METHOD__ . " Rating too high, setting to 5");
+                               $val = '5';
+                               return;
+                       }
+               }
+       }
+       /**
+       * function to validate integers
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateInteger( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-+]?\d+$/D', $val ) ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected integer but got $val" );
+                       $val = null;
+               }
+
+       }
+       /**
+       * function to validate properties with a fixed number of allowed
+       * choices. (closed choice)
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateClosed( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+
+               //check if its in a numeric range
+               $inRange = false;
+               if ( isset( $info['rangeLow'] ) 
+                       && isset( $info['rangeHigh'] )
+                       && is_numeric( $val )
+                       && ( intval( $val ) <= $info['rangeHigh'] )
+                       && ( intval( $val ) >= $info['rangeLow'] )
+               ) {
+                       $inRange = true;
+               }
+
+               if ( !isset( $info['choices'][$val] ) && !$inRange ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected closed choice, but got $val" );
+                       $val = null;
+               }
+       }
+       /**
+       * function to validate and modify flash structure
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateFlash( $info, &$val, $standalone ) {
+               if ( $standalone ) {
+                       // this only validates flash structs, not individual properties
+                       return;
+               }
+               if ( !( isset( $val['Fired'] )
+                       && isset( $val['Function'] )
+                       && isset( $val['Mode'] )
+                       && isset( $val['RedEyeMode'] )
+                       && isset( $val['Return'] )
+               ) ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Flash structure did not have all the required components" );
+                       $val = null;
+               } else {
+                       $val = ( "\0" | ( $val['Fired'] === 'True' )
+                               | ( intval( $val['Return'] ) << 1 )
+                               | ( intval( $val['Mode'] ) << 3 )
+                               | ( ( $val['Function'] === 'True' ) << 5 )
+                               | ( ( $val['RedEyeMode'] === 'True' ) << 6 ) );
+               }
+       }
+       /**
+       * function to validate LangCode properties ( en-GB, etc )
+       *
+       * This is just a naive check to make sure it somewhat looks like a lang code.
+       *
+       * @see rfc 3066
+       * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf page 30 (section 8.2.2.5)
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateLangCode( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               if ( !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $val) ) {
+                       //this is a rather naive check.
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected Lang code but got $val" );
+                       $val = null;
+               }
+
+       }
+       /**
+       * function to validate date properties, and convert to Exif format.
+       *
+       * @param $info Array information about current property
+       * @param &$val Mixed current value to validate. Converts to TS_EXIF as a side-effect.
+       * @param $standalone Boolean if this is a simple property or array
+       */
+       public static function validateDate( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       // this only validates standalone properties, not arrays, etc
+                       return;
+               }
+               $res = array();
+               if ( !preg_match(
+                       /* ahh! scary regex... */
+                       '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D'
+                       , $val, $res)
+               ) {
+                       wfDebugLog( 'XMP', __METHOD__ . " Expected date but got $val" );
+                       $val = null;
+               } else {
+                       /*
+                        * $res is formatted as follows:
+                        * 0 -> full date.
+                        * 1 -> year, 2-> month, 3-> day, 4-> hour, 5-> minute, 6->second
+                        * 7-> Timezone specifier (Z or something like +12:30 )
+                        * many parts are optional, some aren't. For example if you specify
+                        * minute, you must specify hour, day, month, and year but not second or TZ.
+                        */
+
+                       /*
+                        * First of all, if year = 0000, Something is wrongish,
+                        * so don't extract. This seems to happen when
+                        * some programs convert between metadata formats.
+                        */
+                       if ( $res[1] === '0000' ) {
+                               wfDebugLog( 'XMP', __METHOD__ . " Invalid date (year 0): $val" );
+                               $val = null;
+                               return;
+                       }
+                       //if month, etc unspecified, full out as 01.
+                       $res[2] = isset( $res[2] ) ? $res[2] : '01'; //month
+                       $res[3] = isset( $res[3] ) ? $res[3] : '01'; //day
+                       if ( !isset( $res[4] ) ) { //hour
+                               //just have the year month day
+                               $val = $res[1] . ':' . $res[2] . ':' . $res[3];
+                               return;
+                       }
+                       //if hour is set, so is minute or regex above will fail.
+                       //Extra check for empty string necessary due to TZ but no second case.
+                       $res[6] = isset( $res[6] ) && $res[6] != '' ? $res[6] : '00';
+
+                       if ( !isset( $res[7] ) || $res[7] === 'Z' ) {
+                               $val = $res[1] . ':' . $res[2] . ':' . $res[3]
+                                       . ' ' . $res[4] . ':' . $res[5] . ':' . $res[6];
+                               return;
+                       }
+
+                       //do timezone processing. We've already done the case that tz = Z.
+
+                       $unix = wfTimestamp( TS_UNIX, $res[1] . $res[2] . $res[3] . $res[4] . $res[5] . $res[6] );
+                       $offset = intval( substr( $res[7], 1, 2 ) ) * 60 * 60;
+                       $offset += intval( substr( $res[7], 4, 2 ) ) * 60;
+                       if ( substr( $res[7], 0, 1 ) === '-' ) {
+                               $offset = -$offset;
+                       }
+                       $val = wfTimestamp( TS_EXIF, $unix + $offset );
+               }
+
+       }
+       /** function to validate, and more importantly
+        * translate the XMP DMS form of gps coords to
+        * the decimal form we use.
+        *
+        * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf
+        *        section 1.2.7.4 on page 23
+        *
+        * @param $info Array unused (info about prop)
+        * @param &$val String GPS string in either DDD,MM,SSk or
+        *           or DDD,MM.mmk form
+        * @param $standalone Boolean if its a simple prop (should always be true)
+        */
+       public static function validateGPS ( $info, &$val, $standalone ) {
+               if ( !$standalone ) {
+                       return;
+               }
+
+               $m = array();
+               if ( preg_match( 
+                       '/(\d{1,3}),(\d{1,2}),(\d{1,2})([NWSE])/D',
+                       $val, $m )
+               ) {
+                       $coord = intval( $m[1] );
+                       $coord += intval( $m[2] ) * (1/60);
+                       $coord += intval( $m[3] ) * (1/3600);
+                       if ( $m[4] === 'S' || $m[4] === 'W' ) {
+                               $coord = -$coord;
+                       }
+                       $val = $coord;
+                       return;
+               } elseif ( preg_match( 
+                       '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D',
+                       $val, $m )
+               ) {
+                       $coord = intval( $m[1] );
+                       $coord += floatval( $m[2] ) * (1/60);
+                       if ( $m[3] === 'S' || $m[3] === 'W' ) {
+                               $coord = -$coord;
+                       }
+                       $val = $coord;
+                       return;
+
+               } else {
+                       wfDebugLog( 'XMP', __METHOD__ 
+                               . " Expected GPSCoordinate, but got $val." );
+                       $val = null;
+                       return;
+               }
+
+       }
+
+}
index 3834118..4007446 100644 (file)
@@ -3668,12 +3668,14 @@ Variants for Chinese language
 'variantname-tg'      => 'tg', # only translate this message to other languages if you have to change it
 
 # Metadata
-'metadata'          => 'Metadata',
-'metadata-help'     => 'This file contains additional information, probably added from the digital camera or scanner used to create or digitize it.
+'metadata'                  => 'Metadata',
+'metadata-help'             => 'This file contains additional information, probably added from the digital camera or scanner used to create or digitize it.
 If the file has been modified from its original state, some details may not fully reflect the modified file.',
-'metadata-expand'   => 'Show extended details',
-'metadata-collapse' => 'Hide extended details',
-'metadata-fields'   => 'EXIF metadata fields listed in this message will be included on image page display when the metadata table is collapsed.
+'metadata-expand'           => 'Show extended details',
+'metadata-collapse'         => 'Hide extended details',
+'metadata-langitem'         => '\'\'\'$2:\'\'\' $1',
+'metadata-langitem-default' => '$1',
+'metadata-fields'           => 'Image metadata fields listed in this message will be included on image page display when the metadata table is collapsed.
 Others will be hidden by default.
 * make
 * model
@@ -3681,7 +3683,13 @@ Others will be hidden by default.
 * exposuretime
 * fnumber
 * isospeedratings
-* focallength',
+* focallength
+* artist
+* copyright
+* imagedescription
+* gpslatitude
+* gpslongitude
+* gpsaltitude',
 
 # EXIF tags
 'exif-imagewidth'                  => 'Width',
@@ -3696,13 +3704,11 @@ Others will be hidden by default.
 'exif-ycbcrpositioning'            => 'Y and C positioning',
 'exif-xresolution'                 => 'Horizontal resolution',
 'exif-yresolution'                 => 'Vertical resolution',
-'exif-resolutionunit'              => 'Unit of X and Y resolution',
 'exif-stripoffsets'                => 'Image data location',
 'exif-rowsperstrip'                => 'Number of rows per strip',
 'exif-stripbytecounts'             => 'Bytes per compressed strip',
 'exif-jpeginterchangeformat'       => 'Offset to JPEG SOI',
 'exif-jpeginterchangeformatlength' => 'Bytes of JPEG data',
-'exif-transferfunction'            => 'Transfer function',
 'exif-whitepoint'                  => 'White point chromaticity',
 'exif-primarychromaticities'       => 'Chromaticities of primarities',
 'exif-ycbcrcoefficients'           => 'Color space transformation matrix coefficients',
@@ -3719,9 +3725,8 @@ Others will be hidden by default.
 'exif-colorspace'                  => 'Color space',
 'exif-componentsconfiguration'     => 'Meaning of each component',
 'exif-compressedbitsperpixel'      => 'Image compression mode',
-'exif-pixelydimension'             => 'Valid image width',
-'exif-pixelxdimension'             => 'Valid image height',
-'exif-makernote'                   => 'Manufacturer notes',
+'exif-pixelydimension'             => 'Image width',
+'exif-pixelxdimension'             => 'Image height',
 'exif-usercomment'                 => 'User comments',
 'exif-relatedsoundfile'            => 'Related audio file',
 'exif-datetimeoriginal'            => 'Date and time of data generation',
@@ -3736,11 +3741,10 @@ Others will be hidden by default.
 'exif-exposureprogram'             => 'Exposure Program',
 'exif-spectralsensitivity'         => 'Spectral sensitivity',
 'exif-isospeedratings'             => 'ISO speed rating',
-'exif-oecf'                        => 'Optoelectronic conversion factor',
-'exif-shutterspeedvalue'           => 'Shutter speed',
-'exif-aperturevalue'               => 'Aperture',
-'exif-brightnessvalue'             => 'Brightness',
-'exif-exposurebiasvalue'           => 'Exposure bias',
+'exif-shutterspeedvalue'           => 'APEX shutter speed',
+'exif-aperturevalue'               => 'APEX aperture',
+'exif-brightnessvalue'             => 'APEX brightness',
+'exif-exposurebiasvalue'           => 'APEX exposure bias',
 'exif-maxaperturevalue'            => 'Maximum land aperture',
 'exif-subjectdistance'             => 'Subject distance',
 'exif-meteringmode'                => 'Metering mode',
@@ -3750,7 +3754,6 @@ Others will be hidden by default.
 'exif-focallength-format'          => '$1 mm', # only translate this message to other languages if you have to change it
 'exif-subjectarea'                 => 'Subject area',
 'exif-flashenergy'                 => 'Flash energy',
-'exif-spatialfrequencyresponse'    => 'Spatial frequency response',
 'exif-focalplanexresolution'       => 'Focal plane X resolution',
 'exif-focalplaneyresolution'       => 'Focal plane Y resolution',
 'exif-focalplaneresolutionunit'    => 'Focal plane resolution unit',
@@ -3759,7 +3762,6 @@ Others will be hidden by default.
 'exif-sensingmethod'               => 'Sensing method',
 'exif-filesource'                  => 'File source',
 'exif-scenetype'                   => 'Scene type',
-'exif-cfapattern'                  => 'CFA pattern',
 'exif-customrendered'              => 'Custom image processing',
 'exif-exposuremode'                => 'Exposure mode',
 'exif-whitebalance'                => 'White balance',
@@ -3804,12 +3806,77 @@ Others will be hidden by default.
 'exif-gpsareainformation'          => 'Name of GPS area',
 'exif-gpsdatestamp'                => 'GPS date',
 'exif-gpsdifferential'             => 'GPS differential correction',
+'exif-coordinate-format'           => '$1° $2′ $3″ $4',
+'exif-jpegfilecomment'             => 'JPEG file comment',
+'exif-keywords'                    => 'Keywords',
+'exif-worldregioncreated'          => 'World region that the picture was taken in',
+'exif-countrycreated'              => 'Country that the picture was taken in',
+'exif-countrycodecreated'          => 'Code for the country that the picture was taken in',
+'exif-provinceorstatecreated'      => 'Province or state that the picture was taken in',
+'exif-citycreated'                 => 'City that the picture was taken in',
+'exif-sublocationcreated'          => 'Sublocation of the city that the picture was taken in',
+'exif-worldregiondest'             => 'World region shown',
+'exif-countrydest'                 => 'Country shown',
+'exif-countrycodedest'             => 'Code for country shown',
+'exif-provinceorstatedest'         => 'Province or state shown',
+'exif-citydest'                    => 'City shown',
+'exif-sublocationdest'             => 'Sublocation of city shown',
 'exif-objectname'                  => 'Short title',
+'exif-specialinstructions'         => 'Special instructions',
+'exif-headline'                    => 'Headline',
+'exif-credit'                      => 'Credit/Provider',
+'exif-source'                      => 'Source',
+'exif-editstatus'                  => 'Editorial status of image',       
+'exif-urgency'                     => 'Urgency',
+'exif-fixtureidentifier'           => 'Fixture name',
+'exif-locationdest'                => 'Location depicted',
+'exif-locationdestcode'            => 'Code of location depicted',
+'exif-objectcycle'                 => 'Time of day that media is intended for',
+'exif-contact'                     => 'Contact information',
+'exif-writer'                      => 'Writer',
+'exif-languagecode'                => 'Language',
+'exif-iimversion'                  => 'IIM version',
+'exif-iimcategory'                 => 'Category',
+'exif-iimsupplementalcategory'     => 'Supplemental categories',
+'exif-datetimeexpires'             => 'Do not use after',
+'exif-datetimereleased'            => 'Released on',
+'exif-originaltransmissionref'     => 'Original transmission location code',
+'exif-lens'                        => 'Lens used',
+'exif-serialnumber'                => 'Serial number of camera',
+'exif-cameraownername'             => 'Owner of camera',
+'exif-label'                       => 'Label',
+'exif-datetimemetadata'            => 'Date metadata was last modified',
+'exif-nickname'                    => 'Informal name of image',
+'exif-rating'                      => 'Rating (out of 5)',
+'exif-rightscertificate'           => 'Rights management certificate',
+'exif-copyrighted'                 => 'Copyright status',
+'exif-copyrightowner'              => 'Copyright owner',
+'exif-usageterms'                  => 'Usage terms',
+'exif-webstatement'                => 'Online copyright statement',
+'exif-originaldocumentid'          => 'Unique ID of original document',
+'exif-licenseurl'                  => 'URL for copyright license',
+'exif-morepermissionsurl'          => 'Alternative licensing information',
+'exif-attributionurl'              => 'When re-using this work, please link to',
+'exif-preferredattributionname'    => 'When re-using this work, please credit',
+'exif-pngfilecomment'              => 'PNG file comment',
+'exif-disclaimer'                  => 'Disclaimer',
+'exif-contentwarning'              => 'Content warning',
+'exif-giffilecomment'              => 'GIF file comment',
+'exif-intellectualgenre'           => 'Type of item',
+'exif-subjectnewscode'             => 'Subject code',
+'exif-scenecode'                   => 'IPTC scene code',
+'exif-event'                       => 'Event depicted',
+'exif-organisationinimage'         => 'Organization depicted',
+'exif-personinimage'               => 'Person depicted',
+'exif-originalimageheight'         => 'Height of image before it was cropped',
+'exif-originalimagewidth'          => 'Width of image before it was cropped',
+
 
 # Make & model, can be wikified in order to link to the camera and model name
-'exif-make-value'     => '$1', # do not translate or duplicate this message to other languages
-'exif-model-value'    => '$1', # do not translate or duplicate this message to other languages
-'exif-software-value' => '$1', # do not translate or duplicate this message to other languages
+'exif-make-value'             => '$1', # do not translate or duplicate this message to other languages
+'exif-model-value'            => '$1', # do not translate or duplicate this message to other languages
+'exif-software-value'         => '$1', # do not translate or duplicate this message to other languages
+'exif-software-version-value' => '$1 (Version $2)',
 
 # EXIF attributes
 'exif-compression-1' => 'Uncompressed',
@@ -3836,7 +3903,7 @@ Others will be hidden by default.
 'exif-xyresolution-c' => '$1 dpc', # only translate this message to other languages if you have to change it
 
 'exif-colorspace-1'      => 'sRGB', # only translate this message to other languages if you have to change it
-'exif-colorspace-ffff.h' => 'FFFF.H', # only translate this message to other languages if you have to change it
+'exif-colorspace-65535' => 'Uncalibrated', # only translate this message to other languages if you have to change it
 
 'exif-componentsconfiguration-0' => 'does not exist',
 'exif-componentsconfiguration-1' => 'Y', # only translate this message to other languages if you have to change it
@@ -3860,9 +3927,9 @@ Others will be hidden by default.
 
 'exif-meteringmode-0'   => 'Unknown',
 'exif-meteringmode-1'   => 'Average',
-'exif-meteringmode-2'   => 'CenterWeightedAverage',
+'exif-meteringmode-2'   => 'Center weighted average',
 'exif-meteringmode-3'   => 'Spot',
-'exif-meteringmode-4'   => 'MultiSpot',
+'exif-meteringmode-4'   => 'Multi-Spot',
 'exif-meteringmode-5'   => 'Pattern',
 'exif-meteringmode-6'   => 'Partial',
 'exif-meteringmode-255' => 'Other',
@@ -3911,7 +3978,7 @@ Others will be hidden by default.
 'exif-sensingmethod-7' => 'Trilinear sensor',
 'exif-sensingmethod-8' => 'Color sequential linear sensor',
 
-'exif-filesource-3' => 'DSC', # only translate this message to other languages if you have to change it
+'exif-filesource-3' => 'Digital still camera',
 
 'exif-scenetype-1' => 'A directly photographed image',
 
@@ -3961,6 +4028,10 @@ Others will be hidden by default.
 'exif-gpslongitude-e' => 'East longitude',
 'exif-gpslongitude-w' => 'West longitude',
 
+# Pseudotags used for GPSAltitude
+'exif-gpsaltitude-above-sealevel' => '$1 {{plural:$1|meters|meter}} above sea level',
+'exif-gpsaltitude-below-sealevel' => '$1 {{plural:$1|meters|meter}} below sea level',
+
 'exif-gpsstatus-a' => 'Measurement in progress',
 'exif-gpsstatus-v' => 'Measurement interoperability',
 
@@ -3972,10 +4043,85 @@ Others will be hidden by default.
 'exif-gpsspeed-m' => 'Miles per hour',
 'exif-gpsspeed-n' => 'Knots',
 
+# Pseudotags used for GPSDestDistanceRef
+'exif-gpsdestdistance-k' => 'Kilometres',
+'exif-gpsdestdistance-m' => 'Miles',
+'exif-gpsdestdistance-n' => 'Nautical miles',
+
 # Pseudotags used for GPSTrackRef, GPSImgDirectionRef and GPSDestBearingRef
 'exif-gpsdirection-t' => 'True direction',
 'exif-gpsdirection-m' => 'Magnetic direction',
 
+'exif-gpsdop-excellent' => 'Excellent ($1)',
+'exif-gpsdop-good'      => 'Good ($1)',
+'exif-gpsdop-moderate'  => 'Moderate ($1)',
+'exif-gpsdop-fair'      => 'Fair ($1)',
+'exif-gpsdop-poor'      => 'Poor ($1)',
+
+'exif-objectcycle-a' => 'Morning only',
+'exif-objectcycle-p' => 'Evening only',
+'exif-objectcycle-b' => 'Both morning and evening',
+
+'exif-ycbcrpositioning-1' => 'Centered',
+'exif-ycbcrpositioning-2' => 'Co-sited',
+
+'exif-identifier' => 'Identifier',
+# dc stuff
+
+'exif-dc-contributor' => 'Contributors',
+'exif-dc-coverage'    => 'Spatial or temporal scope of media',
+'exif-dc-date'        => 'Date(s)',
+'exif-dc-publisher'   => 'Publisher',
+'exif-dc-relation'    => 'Related media',
+'exif-dc-rights'      => 'Rights',
+'exif-dc-source'      => 'Source media',
+'exif-dc-type'        => 'Type of media',
+
+'exif-copyrighted-true'  => 'Copyrighted',
+'exif-copyrighted-false' => 'Public domain',
+
+'exif-rating-rejected' => 'Rejected',
+
+'exif-isospeedratings-overflow' => 'Greater than 65535',
+
+'exif-maxaperturevalue-value' => '$1 APEX (f/$2)',
+
+'exif-contact-value' => '$1
+
+$2
+<div class="adr">
+$3
+
+$4, $5, $6 $7
+</div>
+$8',
+
+'exif-iimcategory-ace' => 'Arts, culture and enterntainment',
+'exif-iimcategory-clj' => 'Crime and law',
+'exif-iimcategory-dis' => 'Disasters and accidents',
+'exif-iimcategory-fin' => 'Economy and business',
+'exif-iimcategory-edu' => 'Education',
+'exif-iimcategory-evn' => 'Environment',
+'exif-iimcategory-hth' => 'Health',
+'exif-iimcategory-hum' => 'Human interest',
+'exif-iimcategory-lab' => 'Labour',
+'exif-iimcategory-lif' => 'Lifestyle and leisure',
+'exif-iimcategory-pol' => 'Politics',
+'exif-iimcategory-rel' => 'Religion and belief',
+'exif-iimcategory-sci' => 'Science and technology',
+'exif-iimcategory-soi' => 'Social issues',
+'exif-iimcategory-spo' => 'Sports',
+'exif-iimcategory-war' => 'War, conflict and unrest',
+'exif-iimcategory-wea' => 'Weather',
+
+'exif-subjectnewscode-value' => '$2 ($1)',
+
+'exif-urgency-normal' => 'Normal ($1)',
+'exif-urgency-low'    => 'Low ($1)',
+'exif-urgency-high'   => 'High ($1)',
+'exif-urgency-other'  => 'User-defined priority ($1)',
+
+
 # External editor support
 'edit-externally'      => 'Edit this file using an external application',
 'edit-externally-help' => '(See the [http://www.mediawiki.org/wiki/Manual:External_editors setup instructions] for more information)',
index 2194f02..f32bd8d 100644 (file)
@@ -3225,7 +3225,7 @@ Les autres liens sur la même ligne sont considérés comme des exceptions, par
 'exif-planarconfiguration-1' => 'Données contiguës',
 'exif-planarconfiguration-2' => 'Données séparées',
 
-'exif-colorspace-ffff.h' => 'Non calibré',
+'exif-colorspace-65535' => 'Non calibré',
 
 'exif-componentsconfiguration-0' => 'N’existe pas',
 'exif-componentsconfiguration-5' => 'V',
index 75f2e3b..5d33944 100644 (file)
@@ -3242,7 +3242,7 @@ Los ôtros champs seront cachiês per dèfôt.
 'exif-planarconfiguration-1' => 'Balyês ategnentes',
 'exif-planarconfiguration-2' => 'Balyês sèparâs',
 
-'exif-colorspace-ffff.h' => 'Pas calibrâ',
+'exif-colorspace-65535' => 'Pas calibrâ',
 
 'exif-componentsconfiguration-0' => 'Ègziste pas',
 'exif-componentsconfiguration-5' => 'V',
index 84d301a..39caccd 100644 (file)
@@ -3035,7 +3035,7 @@ I collegamenti successivi, sulla stessa riga, sono considerati come eccezioni (o
 'exif-xyresolution-i' => '$1 punti per pollice (dpi)',
 'exif-xyresolution-c' => '$1 punti per centimetro (dpc)',
 
-'exif-colorspace-ffff.h' => 'Non calibrato',
+'exif-colorspace-65535' => 'Non calibrato',
 
 'exif-componentsconfiguration-0' => 'assente',
 
index 0922343..b481409 100644 (file)
@@ -3315,7 +3315,7 @@ Variants for Chinese language
 'exif-planarconfiguration-1' => '点順次フォーマット',
 'exif-planarconfiguration-2' => '面順次フォーマット',
 
-'exif-colorspace-ffff.h' => 'その他',
+'exif-colorspace-65535' => 'その他',
 
 'exif-componentsconfiguration-0' => 'なし',
 
index a17916f..96ea2c1 100644 (file)
@@ -3326,7 +3326,7 @@ Andere velden worden verborgen.
 'exif-planarconfiguration-1' => 'chunky gegevensformaat',
 'exif-planarconfiguration-2' => 'planar gegevensformaat',
 
-'exif-colorspace-ffff.h' => 'Ongekalibreerd',
+'exif-colorspace-65535' => 'Ongekalibreerd',
 
 'exif-componentsconfiguration-0' => 'bestaat niet',
 
index 383df11..143b95b 100644 (file)
@@ -3022,7 +3022,7 @@ Los autres ligams sus la meteissa linha son considerats coma d'excepcions, per e
 'exif-planarconfiguration-1' => 'Donadas atenentas',
 'exif-planarconfiguration-2' => 'Donadas separadas',
 
-'exif-colorspace-ffff.h' => 'Pas calibrat',
+'exif-colorspace-65535' => 'Pas calibrat',
 
 'exif-componentsconfiguration-0' => 'existís pas',
 'exif-componentsconfiguration-5' => 'V',
index d12327b..ede5825 100644 (file)
@@ -2912,7 +2912,7 @@ J'anliure ch'a-i ven-o dapress, ant sla midema riga, as conto për ecession (vis
 'exif-xyresolution-i' => '$1 pont për pòles (dpi)',
 'exif-xyresolution-c' => '$1 pont për centim (dpc)',
 
-'exif-colorspace-ffff.h' => 'Nen calibrà',
+'exif-colorspace-65535' => 'Nen calibrà',
 
 'exif-componentsconfiguration-0' => 'a esist pa',
 
index 63b9792..7a7ff1c 100644 (file)
@@ -3215,6 +3215,10 @@ Varient Option for wikis with variants conversion enabled.',
 {{Identical|Metadata}}',
 'metadata-expand'   => 'On an image description page, there is mostly a table containing data (metadata) about the image. The most important data are shown, but if you click on this link, you can see more data and information. For the link to hide back the less important data, see "[[MediaWiki:Metadata-collapse/{{SUBPAGENAME}}|{{int:metadata-collapse}}]]".',
 'metadata-collapse' => 'On an image description page, there is mostly a table containing data (metadata) about the image. The most important data are shown, but if you click on the link "[[MediaWiki:Metadata-expand/{{SUBPAGENAME}}|{{int:metadata-expand}}]]", you can see more data and information. This message is for the link to hide back the less important data.',
+'metadata-langitem'         => 'This is used for constructing the list of translations when a metadata property is translated into multiple languages.
+
+$1 is the value of the property (in one language), $2 is the language name that this translation is for (or language code if language name cannot be determined), $3 is the language code.',
+'metadata-langitem-default' => 'Similar to "metadata-langitem" but for the case where a multilingual property has a default specified that does not specify what language the default is in. $1 is the value of the property. ',
 'metadata-fields'   => "'''Warning:''' Do not translate list items, only translate the text! So leave \"<tt>* make</tt>\" and the other items exactly as they are.
 
 The sentences are for explanation only and are not shown to the user.",
@@ -3234,15 +3238,20 @@ The sentences are for explanation only and are not shown to the user.",
 'exif-planarconfiguration'         => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-ycbcrsubsampling'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-ycbcrpositioning'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-xresolution'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-yresolution'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-xresolution'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+This is the horizontal resolution in either dots/inch or dots/cm.',
+'exif-yresolution'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+This is the vertical resolution in either dots/inch or dots/cm.',
+'exif-xyresolution-i'              => '{{Optional}} Used to format {{msg-mw|exif-xresolution}} and {{msg-mw|exif-yresolution}} if the unit is dots per inch. $1 is the number of dots/in.',
+'exif-xyresolution-c'              => '{{Optional}} Used to format {{msg-mw|exif-xresolution}} and {{msg-mw|exif-yresolution}} if the unit is dots per centimetre. $1 is the number of dots/cm.',
 'exif-resolutionunit'              => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-stripoffsets'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-rowsperstrip'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-stripbytecounts'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-jpeginterchangeformat'       => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-jpeginterchangeformatlength' => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-transferfunction'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-whitepoint'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-primarychromaticities'       => 'The chromaticity of the three primary colours of the image. Normally this tag is not necessary, since colour space is specified in the colour space information tag. This should probably be translated it as "Chromaticity of primary colours".
 
@@ -3251,30 +3260,65 @@ Exif is a format for storing metadata in image files. See this [http://en.wikipe
 'exif-referenceblackwhite'         => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-datetime'                    => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
+Note, this message is also used for the XMP:ModifyDate property in XMP metadata. See page 35 of http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart1.pdf
+
 Datetime is the time that the digital file was last changed.',
-'exif-imagedescription'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-make'                        => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-model'                       => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-software'                    => 'Short for "The software which was used to create this image".
+'exif-imagedescription'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+This property is the description or caption of the image. It is used for the exif ImageDescription property, the dc:description property in XMP (see http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart1.pdf ), and the iptc-iim 2:120 caption/abstract property ( http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf ).
+
+When an image has multiple differing descriptions, mediawiki follows the MWG guidelines when deciding which to show (Which typically means Exif takes precedence).',
+'exif-make'                        => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+The Manufacturer of the digital camera (or scanner) that took the photo.',
+'exif-model'                       => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+The model of camera (or scanner) used to take the picture.',
+'exif-software'                    => 'Short for "The software which was used to create or modify this image".
+
+The property can come from the Exif Software tag, PNG software chunk, iptc-iim 2:65 Software field, or XMP\'s xmp:CreatorTool field.
 
 Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-artist'                      => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
+This message labels the author or artist of the work. Usually this means who took the photograph, or who drew the picture. The corresponding value field most commonly contains a single author, however it can contain an ordered (or unordered depending on which metadata standard is used to store the information) list of authors. Sometimes the persons position is prefixed before their name such as "Photographer, John Smith". The exif standard recommends multiple authors be specified by "position, Author 1; position for author 2, Author 2\'s name" however this doesn\'t seem to happen in practise very often. If multiple authors are specified using a non-exif standard, then a billeted (or numbered) list is used.
+
+This property can be specified by exif Artist tag, XMP\'s tiff:Artist, XMP\'s dc:creator, iptc-iim\'s 2:80 byline, PNG\'s author textual chunk, PNG\'s (unofficial) artist textual chunk. XMP\'s photoshop:AuthorsPosition and iptc 2:85 byline-title can also affect display of this property.
+
 {{Identical|Author}}',
-'exif-copyright'                   => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-exifversion'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-flashpixversion'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-colorspace'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-componentsconfiguration'     => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-copyright'                   => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Label for information contained in exif Copyright tag, XMP dc:rights, IPTC-iim 2:116, or PNG copyright textual chunk.
+
+Typically the copyright statement for the photograph/drawing/video (such as \'\'(c) 2010 John Smith. Released under GFDL\'\'). Sometimes contains license information. See also {{msg-mw|exif-copyrightowner}}',
+'exif-exifversion'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Version of exif standard photo uses. Typically this is 2.22',
+'exif-flashpixversion'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Version of flashpix used. Flashpix is a format used for storing some types of metadata in image. It is not as commonly used as EXIF, and mediawiki currently cannot read Flashpix data.',
+'exif-colorspace'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+The colorspace of the photo. This tells the computer how to make the colours in the photo be more true to the original photo. Typical values for this are sRGB or uncalibrated. This only gives information on colour information given in the exif-colorspace property. However, colour information is often stored elsewhere in the photo.',
+'exif-componentsconfiguration'     => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+This contains how the information in the picture is stored. This is most commonly Y, Cr, Cb to specify luma, red, blue. RGB is also possible to specify Red, Green, Blue.',
 'exif-compressedbitsperpixel'      => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-pixelydimension'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-pixelxdimension'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-makernote'                   => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-usercomment'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-relatedsoundfile'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-pixelydimension'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+{{Identical|Height}}',
+'exif-pixelxdimension'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+{{Identical|Width}}',
+'exif-usercomment'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Comments by user. Sometimes used like ImageDescription when the ImageDescription contained non-ascii characters. (Technically ImageDescription is supposed to contain ascii characters. In practise utf-8 is used in ImageDescription, so this field isn\'t used too much.)',
+'exif-relatedsoundfile'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Some cameras offer the option to record an audio "memo" for the photo they just took. If the user did that, the name of the file is labelled with this message.',
 'exif-datetimeoriginal'            => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
-The date and time when the original image data was generated.',
+The date and time when the original image data was generated. For example if it was a painting from 1773, scanned in to a computer in 2007, the datetimeoriginal would be 1773 and {{msg-mw|exif-datetimedigitized}} would have the 2007 date.',
 'exif-datetimedigitized'           => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 The date and time when the image was stored as digital data.',
@@ -3287,7 +3331,9 @@ This tag shows the detail of the fraction of a second (1/100s) at which the file
 'exif-subsectimedigitized'         => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 This tag shows the detail of the fraction of a second (1/100s) at which the file was stored as digital data, when the tag {{msg-mw|Exif-datetimedigitized}} is recorded to the whole second.',
-'exif-exposuretime'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-exposuretime'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+The exposure time. Number of (or fraction of) seconds the film was exposed to light. The value for this property is formatted using {{msg-mw|exif-exposuretime-format}}',
 'exif-exposuretime-format'         => "Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 *$1 is the exposure time written as a fraction of a second, for example 1/640 of a second.
@@ -3299,19 +3345,30 @@ The [http://en.wikipedia.org/wiki/F_number F number] is the relative aperture of
 'exif-fnumber-format'              => "{{optional}}
 Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
+{{optional}}
+
 *$1 is a number
 *f is the abbreviation used in English for 'f-number'.",
-'exif-exposureprogram'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-spectralsensitivity'         => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-isospeedratings'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-oecf'                        => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-exposureprogram'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+How the camera figured out what exposure to use. (If it was manually set, if its optimizing for fast shutter speed, etc).',
+'exif-spectralsensitivity'         => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+How sensitive each channel (colour) of the photo is to light. This tag is almost never used.',
+'exif-isospeedratings'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+The iso speed of the film used in the camera. This is basically a measure of how sensitive the film in the camera is to light.',
 'exif-shutterspeedvalue'           => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
-[http://en.wikipedia.org/wiki/Shutter_speed Shutter speed] is the time that the camera shutter is open.',
+[http://en.wikipedia.org/wiki/Shutter_speed Shutter speed] is the time that the camera shutter is open.
+
+This is the shutter speed measured in APEX units (negative base 2 log of shutter speed in seconds). See {{msg-mw|exif-exposuretime}} for this property in more traditional units of seconds.',
 'exif-aperturevalue'               => "Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
-The [http://en.wikipedia.org/wiki/Aperture aperture] of a camera is the hole through which light shines. This message can be translated 'Aperture width'.",
-'exif-brightnessvalue'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+The [http://en.wikipedia.org/wiki/Aperture aperture] of a camera is the hole through which light shines. This message can be translated 'Aperture width'. Note, this is measured in APEX units which is 2*log<sub>2</sub>(f-number) . See {{msg-mw|exif-fnumber}} for this value in more traditional units.",
+'exif-brightnessvalue'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+How intense the illumination of the scene photographed is. Measured in APEX brightness units. See Annex C of Exif standard for details on the measurement system in use.',
 'exif-exposurebiasvalue'           => "Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 Another term for [http://en.wikipedia.org/wiki/Exposure_bias 'exposure bias'] is 'exposure compensation'.",
@@ -3336,12 +3393,16 @@ See this [http://en.wikipedia.org/wiki/Focal_length_(photography) Wikipedia arti
 'exif-focallength-format'          => "{{optional}}
 Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
+{{optional}}
+
 *$1 is a number
 *mm is the abbreviation used in English for the unit of measurement of length 'millimetre'.",
 'exif-subjectarea'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
-This exif property contains the position of the main subject of the picture in pixels from the upper left corner and additionally its width and height in pixels.',
-'exif-flashenergy'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+This exif property contains the position of the main subject. The first two numbers is the position of the subject in the picture in pixels from the upper left corner. If a third number is specified, it is a circle centred at the first two numbers. If four numbers are specified, the first two are coordinates of the centre of the subject as before, the third is the width of the rectangle, and the fourth is the height of the rectangle. It is rare for a photo to use this tag.',
+'exif-flashenergy'                 => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+How bright the flash is in beam candle power seconds.',
 'exif-spatialfrequencyresponse'    => '[http://en.wikipedia.org/wiki/Spatial_frequency Spatial frequency] is the number of edges per degree of the visual angle. The human eye scans the viewed scenary for edges and uses these edges to detect what it sees. Few edges make it hard to recognize the seen objects, but many edges do so too. A rate of about 4 to 6 edges per degree of the viewing range is seen as optimal for the recognition of objects.
 
 Spatial frequency response is a measure for the capability of camera lenses to depict spatial frequencies.',
@@ -3350,11 +3411,17 @@ Spatial frequency response is a measure for the capability of camera lenses to d
 Indicates the number of pixels in the image width (X) direction per FocalPlaneResolutionUnit on the camera focal plane.',
 'exif-focalplaneyresolution'       => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-focalplaneresolutionunit'    => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-subjectlocation'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-subjectlocation'             => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Same as {{msg-mw|exif-subjectarea}} but only ever has two numbers as a value.',
 'exif-exposureindex'               => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
 'exif-sensingmethod'               => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-filesource'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
-'exif-scenetype'                   => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].',
+'exif-filesource'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+Determines if the image was recorded by a digital camera adhering to DSC standard (which is almost all digital cameras).',
+'exif-scenetype'                   => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
+
+If the image is directly photographed (taken by a digital camera).',
 'exif-cfapattern'                  => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 CFA stands for [http://en.wikipedia.org/wiki/Color_filter_array color filter array].',
@@ -3363,7 +3430,7 @@ CFA stands for [http://en.wikipedia.org/wiki/Color_filter_array color filter arr
 See also Wikipedia on [http://en.wikipedia.org/wiki/Image_processing image processing].',
 'exif-exposuremode'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
-See also Wikipedia on [http://en.wikipedia.org/wiki/Exposure_(photography) exposure in photography].',
+See also Wikipedia on [http://en.wikipedia.org/wiki/Exposure_(photography) exposure in photography]. This tag shows if the photo\'s exposure was manually set or automatically determined.',
 'exif-whitebalance'                => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 See also Wikipedia on [http://en.wikipedia.org/wiki/Color_balance color balance].',
@@ -3373,10 +3440,124 @@ See also Wikipedia on [http://en.wikipedia.org/wiki/Digital_zoom digital zoom].'
 'exif-focallengthin35mmfilm'       => 'Exif is a format for storing metadata in image files. See this [http://en.wikipedia.org/wiki/Exchangeable_image_file_format Wikipedia article] and the example at the bottom of [http://commons.wikimedia.org/wiki/File:Phalacrocorax-auritus-020.jpg this page on Commons]. The tags are explained [http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html briefly] and [http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf in further detail].
 
 See also Wikipedia on [http://en.wikipedia.org/wiki/Focal_length#In_photography focal length].',
-'exif-gpslatitude'                 => '{{Identical|Latitude}}',
-'exif-gpslongitude'                => '{{Identical|Longitude}}',
+'exif-imageuniqueid'               => 'A unique identifier for the image in the form of a 128-bit hexadecimal string. See http://www.exif.org/Exif2-2.PDF for details on exif properties.',
+'exif-gpsversionid'                => 'Version of the GPS IFD used to store location information. This is usually 2.2.0.0',
+'exif-gpslatituderef'              => 'In older versions of mediawiki this referred to if the latitude was North or South. This is no longer used in modern versions of mediawiki except for when using a foreign image repository that is using an older version of mediawiki since the information is now contained in {{msg-mw|exif-gpslatitude}}.',
+'exif-gpslongituderef'             => 'Same as {{msg-mw|exif-gpslatituderef}} but for longitude.',
+'exif-gpsaltituderef'              => 'No longer used except for when using foreign image repository with old version of mediawiki. 0 for above sea level, 1 for below sea level.',
+'exif-gpsaltitude'                 => 'Altitude in meters that the image was taken at.',
+'exif-gpstimestamp'                => 'Time (does not include date) that GPS measurement was taken, in UTC. Since often this is at the same time as photo was taken, this is sometimes more reliable than {{msg-mw|exif-datetimeoriginal}}.',
+'exif-gpsdatestamp'                => 'Date (does not generally include time unless recorded in XMP) that GPS measurement was taken, in UTC. Since often this is at the same date as photo was taken, this is sometimes more reliable than {{msg-mw|exif-datetimeoriginal}}.',
+'exif-gpsmeasuremode'              => 'Is the measurement 2D (latitude and longitude) or 3D (latitude, longitude, and altitude).',
+'exif-gpsdop'                      => 'How accurate the GPS information is. See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+'exif-gpslatitude'                 => 'The latitude of the location from where the picture was taken from.
 'exif-objectname'                  => 'This message labels a field in the image metadata table that is a short name or title for the image. (As compared to {{msg-mw|exif-imagedescription}} which is a long description of the image).',
 
+{{Identical|Latitude}}',
+'exif-gpslongitude'                => 'The longitude of the location from where the picture was taken from.
+
+{{Identical|Longitude}}',
+'exif-gpsdestlatitude'             => 'The latitude of the location shown in the picture, if it is different from latitude of the camera location. See {{msg-mw|exif-gpslatitude}}.
+
+{{Identical|Latitude}}',
+'exif-gpsdestlongitude'            => 'The longitude of the location shown in the picture, if it is different from longitude of the camera location. See {{msg-mw|exif-gpslongitude}}.
+
+{{Identical|Longitude}}',
+'exif-coordinate-format'           => '{{optional}} For formatting GPS latitude coordinates. $1 is degrees, $2 is minutes, $3 is seconds (up to two decimal places), $4 is direction (N, S, W, or E), $5 is coordinate as a single positive or negative real number.',
+'exif-jpegfilecomment'             => 'This is not a true exif tag, but the contents of the JPEG COM segment. This often contains a file source, but can potentially contain any comment about the file. This is similar to {{msg-mw|exif-usercomment}}, {{msg-mw|exif-pngfilecomment}}, and {{msg-mw|exif-giffilecomment}}.',
+'exif-keywords'                    => 'List of keywords for the photograph (or other media).
+
+This can come from IPTC-iim 2:25 keyword field, or XMP\'s dc:subject field.',
+'exif-worldregioncreated'          => 'The world region (generally that means continent, but could also include \'World\' as a whole) where the media was created.',
+'exif-countrycreated'              => 'Country that the picture was taken in. Note this is where it was taken, not what country is depicted in the picture.',
+'exif-countrycodecreated'          => 'ISO Code for the country that the picture was taken in. Note this is where it was taken, not what country is depicted in the picture.',
+'exif-provinceorstatecreated'      => 'Province, state, territory, or other secondary political division (bigger than a city, smaller then a country) where that the picture was taken in.  Note this is where it was taken, not what province/state is depicted in the picture.',
+'exif-citycreated'                 => 'City that the picture was taken in.  Note this is where it was taken, not what city is depicted in the picture. This is generally only used if different from the city depicted in photo.',
+'exif-sublocationcreated'          => 'Sub-location of the city that the picture was taken in. This might be a street, a part of town, etc.  Note this is where it was taken, not what sub-location is depicted in the picture.',
+'exif-worldregiondest'             => 'World region shown. This generally means the continent, but could have the value of world as well.',
+'exif-countrydest'                 => 'Country shown. See also {{msg-mw|exif-worldregioncreated}}.',
+'exif-countrycodedest'             => 'ISO Code for country shown',
+'exif-provinceorstatedest'         => 'Province, state, territory, or other secondary political division shown.',
+'exif-citydest'                    => 'City shown',
+'exif-sublocationdest'             => 'Sub-location of city shown. This could be an address, a street, an area of town, etc.',
+'exif-objectname'                  => 'This is a short name for the image or other media. (As compared to {{msg-mw|exif-imagedescription}} which is a long description of the image). This is sometimes an id number used to identify the photo, or a (short) title of the photo.
+
+This property is extracted based on XMP\'s dc:title property ( http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart1.pdf ), PNG\'s title keyword ( http://www.w3.org/TR/PNG/#11keywords ), or IPTC-iim 2:05 Object name property ( http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf ).',
+'exif-specialinstructions'         => 'Special instructions for how to use the image/media. This might include embargo notices, or other warnings.
+
+This is IPTC-iim property 2:40. See http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf for details.',
+'exif-headline'                    => 'A short version of the image caption. The IPTC4XMP standard is clear that "this is not the same thing as title [ {{msg-mw|exif-objectname}} ]".
+
+This is extracted from XMP\'s photoshop:headline ( http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201007_1.pdf ) and IPTC-iim: 2:105 Headline tag ( http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf ).',
+'exif-credit'                      => 'Provider/credit.
+
+Who gave us the image. This might be different from the creator of the image. This is IPTC-iim property 2:110',
+'exif-source'                      => 'See IPTC-iim standard 2:115 - http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf.
+
+This is who originally owned the image (a person, stock photo agency, etc). This does not refer to the image this image is based on.',
+'exif-editstatus'                  => 'Editorial status of image. This is more intended for use with people making news papers. This denotes weather the image is on the main page, is part of a correction, etc. See 2:07 of http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf',
+'exif-urgency'                     => 'Urgency. How urgent this image is. 1 is very urgent, 5 is normal, 8 is  very low priority.',
+'exif-fixtureidentifier'           => 'Fixture name. If this image is part of a regular column in a news paper, name of column goes here.',
+'exif-locationdest'                => 'Full printable name of location.',
+'exif-locationdestcode'            => 'Code of location depicted. Typically this is an ISO country code, but the IPTC-iim standard also defines other codes like XSP for outer space. See appendix D (and tag 2:100) of http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf',
+'exif-objectcycle'                 => 'Time of day that media is intended for. Either morning only, evening only, or all day. Typically only used for news related things that might only be broadcast at a specific time of day. See {{msg-mw|exif-objectcycle-a}}, {{msg-mw|exif-objectcycle-p}} and {{msg-mw|exif-objectcycle-b}} for the values that this message labels.',
+'exif-contact'                     => 'Contact information of the person responsible for the image.',
+'exif-writer'                      => 'The person who wrote the caption of the image. See Description Writer on page 18 of http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201007_1.pdf',
+'exif-languagecode'                => 'Language of image/media.
+
+This is taken from IPTC-iim 2:135 and XMP\'s dc:language.',
+'exif-iimversion'                  => 'IIM version number. Version of information interchange 2:xx records. 4 is current version. 2 is often seen as well. This is the value stored 2:00 field (Note, iptc-iim also stores a model version in 1:00. This version field displays the 2:00 record only)',
+'exif-iimcategory'                 => 'Primary Category of image (or other media). Technically supposed to be limited to 3 characters, however that is not always followed. Some common 3 letter category abbreviations are expanded by mediawiki. Similar to {{msg-mw|exif-keywords}}.',
+'exif-iimsupplementalcategory'     => 'Supplemental categories. Like {{msg-mw|exif-iimcategory}} but for categories beyond the main one.',
+'exif-datetimeexpires'             => 'Date after which not to use the image (media). This is often used in news situations were certain things (like forecasts) should not be used after a specified date.',
+'exif-datetimereleased'            => 'Earliest date the image (media) can be used. See 2:30 of http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf',
+'exif-originaltransmissionref'     => 'This is basically a job id. This could help an individual keep track of for what reason the image was created. See Job Id on page 19 of http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201007_1.pdf ',
+'exif-lens'                        => 'Description of lens used. This is taken from aux:Lens XMP property. See http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart2.pdf',
+'exif-serialnumber'                => 'Serial number of camera. See aux:SerialNumber in http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart2.pdf',
+'exif-cameraownername'             => 'Who owns the camera.',
+'exif-label'                       => 'Label given to the image for organizational purposes. This is very similar to {{msg-mw|exif-keywords}}. Label is more used by a person to organize their media, where keywords are used to describe the photo contents itself.
+
+This property can come from xmp:Label in XMP ( http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart1.pdf ) or the label textual chunk in PNG.',
+'exif-datetimemetadata'            => 'Date metadata was last modified. Typically this refers to XMP metadata.',
+'exif-nickname'                    => 'Short informal name of image. See http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart2.pdf',
+'exif-rating'                      => 'This is a rating for how good the image is. The range is between 1 to 5 (5 highest), with an additional option of "reject".',
+'exif-rightscertificate'           => 'URL of Rights management certificate. This comes from XMPRights:Certificate property. See http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart1.pdf',
+'exif-copyrighted'                 => 'Copyright status. This is a true or false field showing either Copyrighted or Public Domain. It should be noted that Copyrighted includes freely-licensed works.',
+'exif-copyrightowner'              => 'Copyright owner. Can have more than one person or entity.',
+'exif-usageterms'                  => 'Terms under which you\'re allowed to use the image/media.',
+'exif-webstatement'                => 'URL detailing the copyright status of the image, and how you\'re allowed to use the image. Often this is a link to a creative commons license, however the creative commons people recommend using a page that generally contains specific information about the image, and recommend using {{msg-mw|exif-licenseurl}} for linking to the license. See http://wiki.creativecommons.org/XMP',
+'exif-originaldocumentid'          => 'A unique id of the original document (image) that this document (image) is based on.',
+'exif-licenseurl'                  => 'URL for copyright license. This is almost always a creative commons license since this information comes from the creative commons namespace of XMP (but could be a link to any type of license). See also {{msg-mw|exif-webstatement}}',
+'exif-morepermissionsurl'          => 'A url where you can "buy" (or otherwise negotiate) to get more rights for the image.',
+'exif-attributionurl'              => 'A url that you\'re supposed to use when re-using the image.',
+'exif-preferredattributionname'    => 'The preferred name to give credit to when re-using this image.',
+'exif-disclaimer'                  => 'Disclaimer for the image.',
+'exif-contentwarning'              => 'Content warning for the image. For example if the image/media contains violent, sexual or otherwise offensive content.
+
+This comes from the png warning textual chunk. See http://www.w3.org/TR/PNG/#11keywords',
+'exif-intellectualgenre'           => 'The "intellectual genre" of the image/media item. This typically means the type of item it is, ignoring the actual content of the item. See http://cv.iptc.org/newscodes/genre/ for some examples of the types of values this field might have.',
+'exif-subjectnewscode'             => 'A (or multiple) codes describing the content of the image/media. The code is an 8 digit number representing some sort of category. The code is hierarchical , with the first two digits being a broad category (this broad category is shown to the user. See {{msg-mw|exif-subjectnewscode-value}} for how the value this field labels is shown to user). See http://cv.iptc.org/newscodes/subjectcode for the full list of codes.',
+'exif-scenecode'                   => 'IPTC (numeric) scene code. Contains information on what type of scene it is (like panoramic scene, close-up, etc). See http://cv.iptc.org/newscodes/scene/',
+'exif-event'                       => 'The event depicted',
+'exif-organisationinimage'         => 'Name of organisations shown in image',
+'exif-personinimage'               => 'Name of person depicted in image',
+'exif-originalimageheight'         => 'Height of image before it was cropped in pixels
+
+{{identical|Height}}',
+'exif-originalimagewidth'          => 'Width of image before it was cropped in pixels
+
+{{identical|Width}}',
+'exif-identifier'                  => 'A formal identifier for the image. Often this is a URL.',
+'exif-dc-contributor'              => 'People who helped make the resource, but are secondary in contribution to the author.',
+'exif-dc-coverage'                 => '"The extent or scope of the resource" see dc:coverage in http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart2.pdf',
+'exif-dc-date'                     => 'One or more dates associated with the image. How they are associated is not really defined. From the dc:date XMP property.',
+'exif-dc-publisher'                => 'One or more publisher of resource',
+'exif-dc-relation'                 => 'Something related to this image. Often a list of url\'s to related images.',
+'exif-dc-rights'                   => 'Copyright information about the image/media given in informal language.',
+'exif-dc-source'                   => 'Source of the image. This is another image that this image is based on. This does not refer to the person who provided the image.',
+'exif-dc-type'                     => 'Type or genre of image/media. This might be something like painting or photograph.',
+
+
 # EXIF attributes
 'exif-compression-6' => '{{optional}}',
 
@@ -3401,8 +3582,8 @@ CW is an abbreviation for clockwise',
 
 CCW is an abbreviation for counter-clockwise.',
 
-'exif-colorspace-1'      => '{{optional}}',
-'exif-colorspace-ffff.h' => '{{optional}}',
+'exif-colorspace-1'     => '{{Optional}} If it uses the standard sRGB colour space.',
+'exif-colorspace-65535' => 'The photograph is not colour calibrated.',
 
 'exif-componentsconfiguration-1' => '{{optional}}',
 'exif-componentsconfiguration-2' => '{{optional}}',
@@ -3449,8 +3630,6 @@ CCW is an abbreviation for counter-clockwise.',
 'exif-sensingmethod-5' => "''Color sequential'' means, that the three base colors are measured one after another (i.e. the sensor is first measuring red, than green, than blue).",
 'exif-sensingmethod-8' => "''Color sequential'' means, that the three base colors are measured one after another (i.e. the sensor is first measuring red, than green, than blue).",
 
-'exif-filesource-3' => '{{optional}}',
-
 'exif-exposuremode-2' => "A type of exposure mode shown as part of the metadata on image description pages. The Wikipedia article on [http://en.wikipedia.org/wiki/Bracketing#Exposure_bracketing bracketing] says that 'auto bracket' is a camera exposure setting which automatically takes a series of pictures at slightly different light exposures.",
 
 'exif-scenecapturetype-0' => '{{Identical|Standard}}',
@@ -3490,6 +3669,81 @@ Macro view is close-up photography. See [http://en.wikipedia.org/wiki/Macro_phot
 
 # Pseudotags used for GPSSpeedRef
 'exif-gpsspeed-n' => "Knots: ''Knot'' is a unit of speed on water used for ships, etc., equal to one nautical mile per hour.",
+# Pseudotags used for GPSLatitudeRef and GPSDestLatitudeRef
+'exif-gpslatitude-n' => 'Very rarely used. Only used when using an old version of Mediawiki as a foreign image repo.',
+'exif-gpslatitude-s' => 'Very rarely used. Only used when using an old version of Mediawiki as a foreign image repo.',
+
+# Pseudotags used for GPSLongitudeRef and GPSDestLongitudeRef
+'exif-gpslongitude-e' => 'Very rarely used. Only used when using an old version of Mediawiki as a foreign image repo.',
+'exif-gpslongitude-w' => 'Very rarely used. Only used when using an old version of Mediawiki as a foreign image repo.',
+
+'exif-gpsmeasuremode-2' => 'Only latitude and longitude recorded, no altitude.',
+'exif-gpsmeasuremode-3' => 'Latitude, longitude, and altitude recorded.',
+
+'exif-gpsdop-excellent' => '$1 is the actual HDOP/PDOP value (less than or equal to 2 for excellent). See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+'exif-gpsdop-good'      => '$1 is the actual HDOP/PDOP value (2-5 for good). See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+'exif-gpsdop-moderate'  => '$1 is the actual HDOP/PDOP value (5-10 for moderate). See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+'exif-gpsdop-fair'      => '$1 is the actual HDOP/PDOP value (10-20 for fair). See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+'exif-gpsdop-poor'      => '$1 is the actual HDOP/PDOP value (greater than 20 for poor). See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)',
+
+'exif-objectcycle-a' => 'Morning only (a is for AM)',
+'exif-objectcycle-p' => 'Evening only (p is for PM)',
+'exif-objectcycle-b' => 'Both morning and evening (b is for both)',
+
+'exif-ycbcrpositioning-1' => 'If the Chrominance samples are centered with respect to the Luminance samples.',
+'exif-ycbcrpositioning-2' => 'If the Chrominance samples are on top of to the Luminance samples.',
+
+'exif-software-version-value' => 'This is very rarely used, and only with iptc-iim 2:70 property. $1 is the Software name, $2 is its version.',
+
+'exif-copyrighted-true'  => 'The image is under copyright (including if its copyrighted but freely licensed)',
+'exif-copyrighted-false' => 'The image is Public domain',
+
+'exif-rating-rejected' => 'If the rating field has a rating of -1 to mean that the file was totally "rejected"',
+
+'exif-isospeedratings-overflow' => 'Exif can\'t store iso speed ratings beyond 65535. This message is shown if the iso speed is too big to be stored.',
+
+'exif-maxaperturevalue-value' => '{{Optional}}
+$1 is maxaperture in APEX units (APEX aperture units = 2log<sub>2</sub>(f-number) ). $2 is the value in the more traditional f-number units.',
+
+'exif-contact-value' => '{{optional}}
+*$1 is email
+*$2 is URL of website
+*$3 is street address.
+*$4 is city
+*$5 is region
+*$6 is postal code
+*$7 is country
+*$8 is telephone number
+Note, not all fields are guaranteed to be present, some may be empty strings.',
+
+'exif-iimcategory-ace' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-clj' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-dis' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-fin' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-edu' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-evn' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-hth' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-hum' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-lab' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-lif' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-pol' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-rel' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-sci' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-soi' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-spo' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-war' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+'exif-iimcategory-wea' => 'Displayed as part of the iimcategory field if the 3 letter code is recognized, or as part {{msg-mw|exif-subjectnewscode-value}}',
+
+'exif-subjectnewscode-value' => '{{Optional}}
+
+*$1 is numeric IPTC subject news code (one of http://cv.iptc.org/newscodes/subjectcode )
+*$2 is one of 17 broad categories that the code falls into. For example any code starting with 15 has the contents of {{msg-mw|exif-iimcategory-spo}} for $2.',
+
+'exif-urgency-normal' => '$1 is numeric priority (aka 5 for normal)',
+'exif-urgency-low'    => '$1 is numeric priority (6-8 for low)',
+'exif-urgency-high'   => '$1 is numeric priority (1-4 for high)',
+'exif-urgency-other'  => '$1 is numeric priority. Most specs define 0 and 9 to either be reserved or not allowed. However the exiftool documentation defines 0 to be reserved and 9 to be user-defined priority.',
+
 
 # External editor support
 'edit-externally'      => 'Displayed on image description pages. See for example [[:Image:Yes.png#filehistory]].',
index eb30949..645b4e2 100644 (file)
@@ -2652,7 +2652,7 @@ Li lijami succissivi, supra la stissa riga, sunnu cunzidirati comu eccizzioni (p
 'exif-xyresolution-i' => '$1 punti pi puseri (dpi)',
 'exif-xyresolution-c' => '$1 punti pi cintìmitru (dpc)',
 
-'exif-colorspace-ffff.h' => 'Nun calibbratu',
+'exif-colorspace-65535' => 'Nun calibbratu',
 
 'exif-componentsconfiguration-0' => 'assenti',
 
index 7c16635..9f6aba6 100644 (file)
@@ -100,6 +100,7 @@ $wgIgnoredMessages = array(
        'exif-make-value',
        'exif-model-value',
        'exif-software-value',
+       'exif-software-version-value',
        'history_copyright',
        'licenses',
        'loginstart',
@@ -280,18 +281,20 @@ $wgOptionalMessages = array(
        'exif-xyresolution-i',
        'exif-xyresolution-c',
        'exif-colorspace-1',
-       'exif-colorspace-ffff.h',
        'exif-componentsconfiguration-1',
        'exif-componentsconfiguration-2',
        'exif-componentsconfiguration-3',
        'exif-componentsconfiguration-4',
        'exif-componentsconfiguration-5',
        'exif-componentsconfiguration-6',
+       'exif-contact-value',
+       'exif-coordinate-format',
        'exif-lightsource-20',
        'exif-lightsource-21',
        'exif-lightsource-22',
        'exif-lightsource-23',
-       'exif-filesource-3',
+       'exif-maxaperturevalue-value',
+       'exif-subjectnewscode-value',
        'booksources-isbn',
        'sp-contributions-explain',
        'sorbs',
@@ -389,6 +392,8 @@ $wgOptionalMessages = array(
        'shared-repo-name-wikimediacommons',
        'usermessage-template',
        'filepage.css',
+       'metadata-langitem',
+       'metadata-langitem-default',
        'nocookiesforlogin',
 );
 
@@ -412,7 +417,6 @@ $wgEXIFMessages = array(
        'exif-stripbytecounts',
        'exif-jpeginterchangeformat',
        'exif-jpeginterchangeformatlength',
-       'exif-transferfunction',
        'exif-whitepoint',
        'exif-primarychromaticities',
        'exif-ycbcrcoefficients',
@@ -431,7 +435,6 @@ $wgEXIFMessages = array(
        'exif-compressedbitsperpixel',
        'exif-pixelydimension',
        'exif-pixelxdimension',
-       'exif-makernote',
        'exif-usercomment',
        'exif-relatedsoundfile',
        'exif-datetimeoriginal',
@@ -445,7 +448,6 @@ $wgEXIFMessages = array(
        'exif-exposureprogram',
        'exif-spectralsensitivity',
        'exif-isospeedratings',
-       'exif-oecf',
        'exif-shutterspeedvalue',
        'exif-aperturevalue',
        'exif-brightnessvalue',
@@ -458,7 +460,6 @@ $wgEXIFMessages = array(
        'exif-focallength',
        'exif-subjectarea',
        'exif-flashenergy',
-       'exif-spatialfrequencyresponse',
        'exif-focalplanexresolution',
        'exif-focalplaneyresolution',
        'exif-focalplaneresolutionunit',
@@ -467,7 +468,6 @@ $wgEXIFMessages = array(
        'exif-sensingmethod',
        'exif-filesource',
        'exif-scenetype',
-       'exif-cfapattern',
        'exif-customrendered',
        'exif-exposuremode',
        'exif-whitebalance',
@@ -512,6 +512,7 @@ $wgEXIFMessages = array(
        'exif-gpsareainformation',
        'exif-gpsdatestamp',
        'exif-gpsdifferential',
+       'exif-colorspace-65535',
        'exif-compression-1',
        'exif-unknowndate',
        'exif-orientation-1',
@@ -598,6 +599,7 @@ $wgEXIFMessages = array(
        'exif-contrast-0',
        'exif-contrast-1',
        'exif-contrast-2',
+       'exif-filesource-3',
        'exif-saturation-0',
        'exif-saturation-1',
        'exif-saturation-2',
@@ -612,8 +614,8 @@ $wgEXIFMessages = array(
        'exif-gpslatitude-s',
        'exif-gpslongitude-e',
        'exif-gpslongitude-w',
-       'exif-gpsaltitude-0',
-       'exif-gpsaltitude-1',
+       'exif-gpsaltitude-above-sealevel',
+       'exif-gpsaltitude-below-sealevel',
        'exif-gpsstatus-a',
        'exif-gpsstatus-v',
        'exif-gpsmeasuremode-2',
@@ -626,5 +628,112 @@ $wgEXIFMessages = array(
        'exif-gpsdestdistance-n',
        'exif-gpsdirection-t',
        'exif-gpsdirection-m',
+       'exif-gpsdop-excellent',
+       'exif-gpsdop-good',
+       'exif-gpsdop-moderate',
+       'exif-gpsdop-fair',
+       'exif-gpsdop-poor',
+       'exif-ycbcrpositioning-1',
+       'exif-ycbcrpositioning-2',
+       //non-exif metadata that is still image metadata
+       'exif-jpegfilecomment',
+       'exif-keywords',
+       'exif-worldregioncreated',
+       'exif-countrycreated',
+       'exif-countrycodecreated',
+       'exif-provinceorstatecreated',
+       'exif-citycreated',
+       'exif-sublocationcreated',
+       'exif-worldregiondest',
+       'exif-countrydest',
+       'exif-countrycodedest',
+       'exif-stateorprovincedest',
+       'exif-citydest',
+       'exif-sublocationdest',
        'exif-objectname',
+       'exif-specialinstructions',
+       'exif-headline',
+       'exif-credit',
+       'exif-source',
+       'exif-editstatus',
+       'exif-urgency',
+       'exif-fixtureidentifier',
+       'exif-locationdest',
+       'exif-locationdestcode',
+       'exif-objectcycle',
+       'exif-objectcycle-a',
+       'exif-objectcycle-p',
+       'exif-objectcycle-b',
+       'exif-contact',
+       'exif-writer',
+       'exif-languagecode',
+       'exif-iimversion',
+       'exif-iimcategory',
+       'exif-iimsupplementalcategory',
+       'exif-datetimeexpires',
+       'exif-datetimereleased',
+       'exif-originaltransmissionref',
+       'exif-identifier',
+       'exif-dc-contributor',
+       'exif-dc-coverage',
+       'exif-dc-date',
+       'exif-dc-publisher',
+       'exif-dc-relation',
+       'exif-dc-rights',
+       'exif-dc-source',
+       'exif-dc-type',
+       'exif-lens',
+       'exif-serialnumber',
+       'exif-cameraownername',
+       'exif-label',                      
+       'exif-datetimemetadata',
+       'exif-nickname',                   
+       'exif-rating',                     
+       'exif-rightscertificate',
+       'exif-copyrighted',
+       'exif-copyrightowner',             
+       'exif-usageterms',
+       'exif-webstatement',
+       'exif-originaldocumentid',
+       'exif-licenseurl',
+       'exif-morepermissionsurl',
+       'exif-attributionurl',
+       'exif-preferredattributionname',
+       'exif-copyrighted-true',
+       'exif-copyrighted-false',
+       'exif-rating-rejected',
+       'exif-isospeedratings-overflow',
+       'exif-pngfilecomment',
+       'exif-disclaimer',
+       'exif-contentwarning',
+       'exif-giffilecomment',
+       'exif-intellectualgenre',
+       'exif-iimcategory-ace',
+       'exif-iimcategory-clj',
+       'exif-iimcategory-dis',
+       'exif-iimcategory-fin',
+       'exif-iimcategory-edu',
+       'exif-iimcategory-evn',
+       'exif-iimcategory-hth',
+       'exif-iimcategory-hum',
+       'exif-iimcategory-lab',
+       'exif-iimcategory-lif',
+       'exif-iimcategory-pol',
+       'exif-iimcategory-rel',
+       'exif-iimcategory-sci',
+       'exif-iimcategory-soi',
+       'exif-iimcategory-spo',
+       'exif-iimcategory-war',
+       'exif-iimcategory-wea',
+       'exif-subjectnewscode',
+       'exif-scenecode',
+       'exif-event',
+       'exif-organisationinimage',
+       'exif-personinimage',
+       'exif-originalimageheight',
+       'exif-originalimagewidth',
+       'exif-urgency-normal',
+       'exif-urgency-low',
+       'exif-urgency-high',
+       'exif-urgency-other',
 );
index 83c86ea..b963abc 100644 (file)
@@ -2631,6 +2631,8 @@ $wgMessageStructure = array(
                'metadata-expand',
                'metadata-collapse',
                'metadata-fields',
+               'metadata-langitem',
+               'metadata-langitem-default',
        ),
        'exif' => array(
                'exif-imagewidth',
@@ -2651,7 +2653,6 @@ $wgMessageStructure = array(
                'exif-stripbytecounts',
                'exif-jpeginterchangeformat',
                'exif-jpeginterchangeformatlength',
-               'exif-transferfunction',
                'exif-whitepoint',
                'exif-primarychromaticities',
                'exif-ycbcrcoefficients',
@@ -2670,7 +2671,6 @@ $wgMessageStructure = array(
                'exif-compressedbitsperpixel',
                'exif-pixelydimension',
                'exif-pixelxdimension',
-               'exif-makernote',
                'exif-usercomment',
                'exif-relatedsoundfile',
                'exif-datetimeoriginal',
@@ -2685,7 +2685,6 @@ $wgMessageStructure = array(
                'exif-exposureprogram',
                'exif-spectralsensitivity',
                'exif-isospeedratings',
-               'exif-oecf',
                'exif-shutterspeedvalue',
                'exif-aperturevalue',
                'exif-brightnessvalue',
@@ -2699,7 +2698,6 @@ $wgMessageStructure = array(
                'exif-focallength-format',
                'exif-subjectarea',
                'exif-flashenergy',
-               'exif-spatialfrequencyresponse',
                'exif-focalplanexresolution',
                'exif-focalplaneyresolution',
                'exif-focalplaneresolutionunit',
@@ -2708,7 +2706,6 @@ $wgMessageStructure = array(
                'exif-sensingmethod',
                'exif-filesource',
                'exif-scenetype',
-               'exif-cfapattern',
                'exif-customrendered',
                'exif-exposuremode',
                'exif-whitebalance',
@@ -2753,17 +2750,87 @@ $wgMessageStructure = array(
                'exif-gpsareainformation',
                'exif-gpsdatestamp',
                'exif-gpsdifferential',
+               'exif-jpegfilecomment',
+               'exif-keywords',
+               'exif-worldregioncreated',
+               'exif-countrycreated',
+               'exif-countrycodecreated',
+               'exif-provinceorstatecreated',
+               'exif-citycreated',
+               'exif-sublocationcreated',
+               'exif-worldregiondest',
+               'exif-countrydest',
+               'exif-countrycodedest',
+               'exif-provinceorstatedest',
+               'exif-citydest',
+               'exif-sublocationdest',
                'exif-objectname',
+               'exif-specialinstructions',
+               'exif-headline',
+               'exif-credit',
+               'exif-source',
+               'exif-editstatus',
+               'exif-urgency',
+               'exif-fixtureidentifier',
+               'exif-locationdest',
+               'exif-locationdestcode',
+               'exif-objectcycle',
+               'exif-contact',
+               'exif-writer',
+               'exif-languagecode',
+               'exif-iimversion',
+               'exif-iimcategory',
+               'exif-iimsupplementalcategory',
+               'exif-datetimeexpires',
+               'exif-datetimereleased',
+               'exif-originaltransmissionref',
+               'exif-identifier',
+               'exif-lens',
+               'exif-serialnumber',
+               'exif-cameraownername',
+               'exif-label',
+               'exif-datetimemetadata',
+               'exif-nickname',
+               'exif-rating',
+               'exif-rightscertificate',
+               'exif-copyrighted',
+               'exif-copyrightowner',
+               'exif-usageterms',
+               'exif-webstatement',
+               'exif-originaldocumentid',
+               'exif-licenseurl',
+               'exif-morepermissionsurl',
+               'exif-attributionurl',
+               'exif-preferredattributionname',
+               'exif-pngfilecomment',
+               'exif-disclaimer',
+               'exif-contentwarning',
+               'exif-giffilecomment',
+               'exif-intellectualgenre',
+               'exif-subjectnewscode',
+               'exif-scenecode',
+               'exif-event',
+               'exif-organisationinimage',
+               'exif-personinimage',
+               'exif-originalimageheight',
+               'exif-originalimagewidth',
        ),
        'exif-values' => array(
                'exif-make-value',
                'exif-model-value',
                'exif-software-value',
+               'exif-software-version-value',
+               'exif-contact-value',
+               'exif-subjectnewscode-value',
        ),
        'exif-compression' => array(
                'exif-compression-1',
                'exif-compression-6',
        ),
+       'exif-copyrighted' => array(
+               'exif-copyrighted-true',
+               'exif-copyrighted-false',
+       ),
        'exif-photometricinterpretation' => array(
                'exif-photometricinterpretation-2',
                'exif-photometricinterpretation-6',
@@ -2791,7 +2858,7 @@ $wgMessageStructure = array(
        ),
        'exif-colorspace' => array(
                'exif-colorspace-1',
-               'exif-colorspace-ffff.h',
+               'exif-colorspace-65535',
        ),
        'exif-componentsconfiguration' => array(
                'exif-componentsconfiguration-0',
@@ -2934,6 +3001,10 @@ $wgMessageStructure = array(
                'exif-gpslongitude-e',
                'exif-gpslongitude-w',
        ),
+       'exif-altituderef' => array(
+               'exif-gpsaltitude-above-sealevel',
+               'exif-gpsaltitude-below-sealevel',
+       ),
        'exif-gpsstatus' => array(
                'exif-gpsstatus-a',
                'exif-gpsstatus-v',
@@ -2947,10 +3018,75 @@ $wgMessageStructure = array(
                'exif-gpsspeed-m',
                'exif-gpsspeed-n',
        ),
+       'exif-gpsdestdistanceref' => array(
+               'exif-gpsdestdistance-k',
+               'exif-gpsdestdistance-m',
+               'exif-gpsdestdistance-n',
+       ),
+       'exif-gdop' => array(
+               'exif-gpsdop-excellent',
+               'exif-gpsdop-good',
+               'exif-gpsdop-moderate',
+               'exif-gpsdop-fair',
+               'exif-gpsdop-poor',
+       ),
+       'exif-objectcycle' => array(
+               'exif-objectcycle-a',
+               'exif-objectcycle-p',
+               'exif-objectcycle-b',
+       ),
        'exif-gpsdirection' => array(
                'exif-gpsdirection-t',
                'exif-gpsdirection-m',
        ),
+       'exif-ycbcrpositioning' => array(
+               'exif-ycbcrpositioning-1',
+               'exif-ycbcrpositioning-2',
+       ),
+       'exif-dc' => array(
+               'exif-dc-contributor',
+               'exif-dc-coverage',
+               'exif-dc-date',
+               'exif-dc-publisher',
+               'exif-dc-relation',
+               'exif-dc-rights',
+               'exif-dc-source',
+               'exif-dc-type',
+       ),
+       'exif-rating' => array(
+               'exif-rating-rejected',
+       ),
+       'exif-isospeedratings' => array(
+               'exif-isospeedratings-overflow',
+       ),
+       'exif-maxaperturevalue' => array(
+               'exif-maxaperturevalue-value',
+       ),
+       'exif-iimcategory' => array(
+               'exif-iimcategory-ace',
+               'exif-iimcategory-clj',
+               'exif-iimcategory-dis',
+               'exif-iimcategory-fin',
+               'exif-iimcategory-edu',
+               'exif-iimcategory-evn',
+               'exif-iimcategory-hth',
+               'exif-iimcategory-hum',
+               'exif-iimcategory-lab',
+               'exif-iimcategory-lif',
+               'exif-iimcategory-pol',
+               'exif-iimcategory-rel',
+               'exif-iimcategory-sci',
+               'exif-iimcategory-soi',
+               'exif-iimcategory-spo',
+               'exif-iimcategory-war',
+               'exif-iimcategory-wea',
+       ),
+       'exif-urgency' => array(
+               'exif-urgency-normal',
+               'exif-urgency-low',
+               'exif-urgency-high',
+               'exif-urgency-other',
+       ),
        'edit-externally' => array(
                'edit-externally',
                'edit-externally-help',
@@ -3445,9 +3581,11 @@ Variants for Chinese language",
        'exif-subjectdistancerange'      => '',
        'exif-gpslatitude'               => 'Pseudotags used for GPSLatitudeRef and GPSDestLatitudeRef',
        'exif-gpslongitude'              => 'Pseudotags used for GPSLongitudeRef and GPSDestLongitudeRef',
+       'exif-altituderef'               => 'Pseudotags used for GPSAltitudeRef',
        'exif-gpsstatus'                 => '',
        'exif-gpsmeasuremode'            => '',
        'exif-gpsspeed'                  => 'Pseudotags used for GPSSpeedRef',
+       'exif-gpsdestdistanceref'        => 'Pseudotags used for GPSDestDistanceRef',
        'exif-gpsdirection'              => 'Pseudotags used for GPSTrackRef, GPSImgDirectionRef and GPSDestBearingRef',
        'edit-externally'       => 'External editor support',
        'all'                   => "'all' in various places, this might be different for inflected languages",
index 15328bf..601bfff 100644 (file)
@@ -42,6 +42,10 @@ class ImageBuilder extends Maintenance {
        function __construct() {
                parent::__construct();
 
+               global $wgUpdateCompatibleMetadata;
+               //make sure to update old, but compatible img_metadata fields.
+               $wgUpdateCompatibleMetadata = true;
+
                $this->mDescription = 'Script to update image metadata records';
 
                $this->addOption( 'missing', 'Check for files without associated database record' );
index 5de18f5..c9a3bd3 100644 (file)
@@ -445,7 +445,7 @@ table.mw_metadata {
        font-size: 0.8em;
        margin-left: 0.5em;
        margin-bottom: 0.5em;
-       width: 300px;
+       width: 400px;
 }
 
 table.mw_metadata caption {
@@ -468,8 +468,8 @@ table.mw_metadata {
 table.mw_metadata td, table.mw_metadata th {
        text-align: center;
        border: 1px solid #aaaaaa;
-       padding-left: 0.1em;
-       padding-right: 0.1em;
+       padding-left: 5px;
+       padding-right: 5px;
 }
 
 table.mw_metadata th {
@@ -480,6 +480,14 @@ table.mw_metadata td {
        background-color: #fcfcfc;
 }
 
+table.mw_metadata ul.metadata-langlist {
+       list-style-type: none;
+       list-style-image: none;
+       padding-right: 5px;
+       padding-left: 5px;
+       margin: 0;
+}
+
 /* Galleries */
 /* These display attributes look nonsensical, but are needed to support IE and FF2 */
 /* Don't forget to update commonPrint.css */