From c1f9b1f7b1b77776192048005dcc66dcf3df2bfb Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 27 Dec 2014 15:41:37 +0100 Subject: Update to MediaWiki 1.24.1 --- includes/media/BMP.php | 14 +- includes/media/Bitmap.php | 488 ++---- includes/media/BitmapMetadataHandler.php | 58 +- includes/media/Bitmap_ClientOnly.php | 15 +- includes/media/DjVu.php | 140 +- includes/media/DjVuImage.php | 82 +- includes/media/Exif.php | 511 +++--- includes/media/ExifBitmap.php | 74 +- includes/media/FormatMetadata.php | 2061 ++++++++++++++--------- includes/media/GIF.php | 48 +- includes/media/GIFMetadataExtractor.php | 51 +- includes/media/IPTC.php | 47 +- includes/media/ImageHandler.php | 72 +- includes/media/Jpeg.php | 94 +- includes/media/JpegMetadataExtractor.php | 24 +- includes/media/MediaHandler.php | 415 +++-- includes/media/MediaTransformOutput.php | 98 +- includes/media/PNG.php | 50 +- includes/media/PNGMetadataExtractor.php | 92 +- includes/media/SVG.php | 202 ++- includes/media/SVGMetadataExtractor.php | 92 +- includes/media/Tiff.php | 24 +- includes/media/TransformationalImageHandler.php | 593 +++++++ includes/media/XCF.php | 141 +- includes/media/XMP.php | 213 ++- includes/media/XMPInfo.php | 992 +++++------ includes/media/XMPValidate.php | 82 +- 27 files changed, 4217 insertions(+), 2556 deletions(-) create mode 100644 includes/media/TransformationalImageHandler.php (limited to 'includes/media') diff --git a/includes/media/BMP.php b/includes/media/BMP.php index 99b7741a..d8b0ba64 100644 --- a/includes/media/BMP.php +++ b/includes/media/BMP.php @@ -28,9 +28,8 @@ * @ingroup Media */ class BmpHandler extends BitmapHandler { - /** - * @param $file + * @param File $file * @return bool */ function mustRender( $file ) { @@ -40,9 +39,9 @@ class BmpHandler extends BitmapHandler { /** * Render files as PNG * - * @param $text - * @param $mime - * @param $params + * @param string $text + * @param string $mime + * @param array $params * @return array */ function getThumbType( $text, $mime, $params = null ) { @@ -52,8 +51,8 @@ class BmpHandler extends BitmapHandler { /** * Get width and height from the bmp header. * - * @param $image - * @param $filename + * @param File $image + * @param string $filename * @return array */ function getImageSize( $image, $filename ) { @@ -75,6 +74,7 @@ class BmpHandler extends BitmapHandler { } catch ( MWException $e ) { return false; } + return array( $w[1], $h[1] ); } } diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 79b0497d..e81b37de 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -26,204 +26,17 @@ * * @ingroup Media */ -class BitmapHandler extends ImageHandler { - /** - * @param $image File - * @param array $params Transform parameters. Entries with the keys 'width' - * and 'height' are the respective screen width and height, while the keys - * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions. - * @return bool - */ - function normaliseParams( $image, &$params ) { - if ( !parent::normaliseParams( $image, $params ) ) { - return false; - } - - # Obtain the source, pre-rotation dimensions - $srcWidth = $image->getWidth( $params['page'] ); - $srcHeight = $image->getHeight( $params['page'] ); - - # Don't make an image bigger than the source - if ( $params['physicalWidth'] >= $srcWidth ) { - $params['physicalWidth'] = $srcWidth; - $params['physicalHeight'] = $srcHeight; - - # Skip scaling limit checks if no scaling is required - # due to requested size being bigger than source. - if ( !$image->mustRender() ) { - return true; - } - } - - # Check if the file is smaller than the maximum image area for thumbnailing - $checkImageAreaHookResult = null; - wfRunHooks( 'BitmapHandlerCheckImageArea', array( $image, &$params, &$checkImageAreaHookResult ) ); - if ( is_null( $checkImageAreaHookResult ) ) { - global $wgMaxImageArea; - - if ( $srcWidth * $srcHeight > $wgMaxImageArea && - !( $image->getMimeType() == 'image/jpeg' && - self::getScalerType( false, false ) == 'im' ) ) { - # Only ImageMagick can efficiently downsize jpg images without loading - # the entire file in memory - return false; - } - } else { - return $checkImageAreaHookResult; - } - - return true; - } - - /** - * Extracts the width/height if the image will be scaled before rotating - * - * This will match the physical size/aspect ratio of the original image - * prior to application of the rotation -- so for a portrait image that's - * stored as raw landscape with 90-degress rotation, the resulting size - * will be wider than it is tall. - * - * @param array $params Parameters as returned by normaliseParams - * @param int $rotation The rotation angle that will be applied - * @return array ($width, $height) array - */ - public function extractPreRotationDimensions( $params, $rotation ) { - if ( $rotation == 90 || $rotation == 270 ) { - # We'll resize before rotation, so swap the dimensions again - $width = $params['physicalHeight']; - $height = $params['physicalWidth']; - } else { - $width = $params['physicalWidth']; - $height = $params['physicalHeight']; - } - return array( $width, $height ); - } - - /** - * @param $image File - * @param $dstPath - * @param $dstUrl - * @param $params - * @param int $flags - * @return MediaTransformError|ThumbnailImage|TransformParameterError - */ - function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { - if ( !$this->normaliseParams( $image, $params ) ) { - return new TransformParameterError( $params ); - } - # Create a parameter array to pass to the scaler - $scalerParams = array( - # The size to which the image will be resized - 'physicalWidth' => $params['physicalWidth'], - 'physicalHeight' => $params['physicalHeight'], - 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}", - # The size of the image on the page - 'clientWidth' => $params['width'], - 'clientHeight' => $params['height'], - # Comment as will be added to the Exif of the thumbnail - 'comment' => isset( $params['descriptionUrl'] ) ? - "File source: {$params['descriptionUrl']}" : '', - # Properties of the original image - 'srcWidth' => $image->getWidth(), - 'srcHeight' => $image->getHeight(), - 'mimeType' => $image->getMimeType(), - 'dstPath' => $dstPath, - 'dstUrl' => $dstUrl, - ); - - # Determine scaler type - $scaler = self::getScalerType( $dstPath ); - - wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} thumbnail at $dstPath using scaler $scaler\n" ); - - if ( !$image->mustRender() && - $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] - && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] ) { - - # normaliseParams (or the user) wants us to return the unscaled image - wfDebug( __METHOD__ . ": returning unscaled image\n" ); - return $this->getClientScalingThumbnailImage( $image, $scalerParams ); - } - - if ( $scaler == 'client' ) { - # Client-side image scaling, use the source URL - # Using the destination URL in a TRANSFORM_LATER request would be incorrect - return $this->getClientScalingThumbnailImage( $image, $scalerParams ); - } - - if ( $flags & self::TRANSFORM_LATER ) { - wfDebug( __METHOD__ . ": Transforming later per flags.\n" ); - $params = array( - 'width' => $scalerParams['clientWidth'], - 'height' => $scalerParams['clientHeight'] - ); - return new ThumbnailImage( $image, $dstUrl, false, $params ); - } - - # Try to make a target path for the thumbnail - if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { - wfDebug( __METHOD__ . ": Unable to create thumbnail destination directory, falling back to client scaling\n" ); - return $this->getClientScalingThumbnailImage( $image, $scalerParams ); - } - - # Transform functions and binaries need a FS source file - $scalerParams['srcPath'] = $image->getLocalRefPath(); - - # Try a hook - $mto = null; - wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) ); - if ( !is_null( $mto ) ) { - wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" ); - $scaler = 'hookaborted'; - } - - switch ( $scaler ) { - case 'hookaborted': - # Handled by the hook above - $err = $mto->isError() ? $mto : false; - break; - case 'im': - $err = $this->transformImageMagick( $image, $scalerParams ); - break; - case 'custom': - $err = $this->transformCustom( $image, $scalerParams ); - break; - case 'imext': - $err = $this->transformImageMagickExt( $image, $scalerParams ); - break; - case 'gd': - default: - $err = $this->transformGd( $image, $scalerParams ); - break; - } - - # Remove the file if a zero-byte thumbnail was created, or if there was an error - $removed = $this->removeBadFile( $dstPath, (bool)$err ); - if ( $err ) { - # transform returned MediaTransforError - return $err; - } elseif ( $removed ) { - # Thumbnail was zero-byte and had to be removed - return new MediaTransformError( 'thumbnail_error', - $scalerParams['clientWidth'], $scalerParams['clientHeight'] ); - } elseif ( $mto ) { - return $mto; - } else { - $params = array( - 'width' => $scalerParams['clientWidth'], - 'height' => $scalerParams['clientHeight'] - ); - return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); - } - } +class BitmapHandler extends TransformationalImageHandler { /** * Returns which scaler type should be used. Creates parent directories * for $dstPath and returns 'client' on error * - * @return string client,im,custom,gd + * @param string $dstPath + * @param bool $checkDstPath + * @return string|Callable One of client, im, custom, gd, imext or an array( object, method ) */ - protected static function getScalerType( $dstPath, $checkDstPath = true ) { + protected function getScalerType( $dstPath, $checkDstPath = true ) { global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand; if ( !$dstPath && $checkDstPath ) { @@ -242,39 +55,21 @@ class BitmapHandler extends ImageHandler { } else { $scaler = 'client'; } - return $scaler; - } - /** - * Get a ThumbnailImage that respresents an image that will be scaled - * client side - * - * @param $image File File associated with this thumbnail - * @param array $scalerParams Array with scaler params - * @return ThumbnailImage - * - * @todo fixme: no rotation support - */ - protected function getClientScalingThumbnailImage( $image, $scalerParams ) { - $params = array( - 'width' => $scalerParams['clientWidth'], - 'height' => $scalerParams['clientHeight'] - ); - return new ThumbnailImage( $image, $image->getURL(), null, $params ); + return $scaler; } /** * Transform an image using ImageMagick * - * @param $image File File associated with this thumbnail + * @param File $image File associated with this thumbnail * @param array $params Array with scaler params * * @return MediaTransformError Error object if error occurred, false (=no error) otherwise */ protected function transformImageMagick( $image, $params ) { # use ImageMagick - global $wgSharpenReductionThreshold, $wgSharpenParameter, - $wgMaxAnimatedGifArea, + global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea, $wgImageMagickTempDir, $wgImageMagickConvertCommand; $quality = array(); @@ -284,18 +79,19 @@ class BitmapHandler extends ImageHandler { $animation_post = array(); $decoderHint = array(); if ( $params['mimeType'] == 'image/jpeg' ) { - $quality = array( '-quality', '80' ); // 80% + $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null; + $quality = array( '-quality', $qualityVal ?: '80' ); // 80% # Sharpening, see bug 6193 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) - / ( $params['srcWidth'] + $params['srcHeight'] ) - < $wgSharpenReductionThreshold ) { + / ( $params['srcWidth'] + $params['srcHeight'] ) + < $wgSharpenReductionThreshold + ) { $sharpen = array( '-sharpen', $wgSharpenParameter ); } if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) { // JPEG decoder hint to reduce memory, available since IM 6.5.6-2 $decoderHint = array( '-define', "jpeg:size={$params['physicalDimensions']}" ); } - } elseif ( $params['mimeType'] == 'image/png' ) { $quality = array( '-quality', '95' ); // zlib 9, adaptive filtering @@ -304,7 +100,6 @@ class BitmapHandler extends ImageHandler { // Extract initial frame only; we're so big it'll // be a total drag. :P $scene = 0; - } elseif ( $this->isAnimatedImage( $image ) ) { // Coalesce is needed to scale animated GIFs properly (bug 1017). $animation_pre = array( '-coalesce' ); @@ -315,7 +110,30 @@ class BitmapHandler extends ImageHandler { } } } elseif ( $params['mimeType'] == 'image/x-xcf' ) { - $animation_post = array( '-layers', 'merge' ); + // Before merging layers, we need to set the background + // to be transparent to preserve alpha, as -layers merge + // merges all layers on to a canvas filled with the + // background colour. After merging we reset the background + // to be white for the default background colour setting + // in the PNG image (which is used in old IE) + $animation_pre = array( + '-background', 'transparent', + '-layers', 'merge', + '-background', 'white', + ); + wfSuppressWarnings(); + $xcfMeta = unserialize( $image->getMetadata() ); + wfRestoreWarnings(); + if ( $xcfMeta + && isset( $xcfMeta['colorType'] ) + && $xcfMeta['colorType'] === 'greyscale-alpha' + && version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0 + ) { + // bug 66323 - Greyscale images not rendered properly. + // So only take the "red" channel. + $channelOnly = array( '-channel', 'R', '-separate' ); + $animation_pre = array_merge( $animation_pre, $channelOnly ); + } } // Use one thread only, to avoid deadlock bugs on OOM @@ -358,7 +176,8 @@ class BitmapHandler extends ImageHandler { if ( $retval !== 0 ) { $this->logErrorForExternalProcess( $retval, $err, $cmd ); - return $this->getMediaTransformError( $params, $err ); + + return $this->getMediaTransformError( $params, "$err\nError code: $retval" ); } return false; # No error @@ -367,7 +186,7 @@ class BitmapHandler extends ImageHandler { /** * Transform an image using the Imagick PHP extension * - * @param $image File File associated with this thumbnail + * @param File $image File associated with this thumbnail * @param array $params Array with scaler params * * @return MediaTransformError Error object if error occurred, false (=no error) otherwise @@ -382,13 +201,15 @@ class BitmapHandler extends ImageHandler { if ( $params['mimeType'] == 'image/jpeg' ) { // Sharpening, see bug 6193 if ( ( $params['physicalWidth'] + $params['physicalHeight'] ) - / ( $params['srcWidth'] + $params['srcHeight'] ) - < $wgSharpenReductionThreshold ) { + / ( $params['srcWidth'] + $params['srcHeight'] ) + < $wgSharpenReductionThreshold + ) { // Hack, since $wgSharpenParamater is written specifically for the command line convert list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter ); $im->sharpenImage( $radius, $sigma ); } - $im->setCompressionQuality( 80 ); + $qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null; + $im->setCompressionQuality( $qualityVal ?: 80 ); } elseif ( $params['mimeType'] == 'image/png' ) { $im->setCompressionQuality( 95 ); } elseif ( $params['mimeType'] == 'image/gif' ) { @@ -432,19 +253,17 @@ class BitmapHandler extends ImageHandler { return $this->getMediaTransformError( $params, "Unable to write thumbnail to {$params['dstPath']}" ); } - } catch ( ImagickException $e ) { return $this->getMediaTransformError( $params, $e->getMessage() ); } return false; - } /** * Transform an image using a custom command * - * @param $image File File associated with this thumbnail + * @param File $image File associated with this thumbnail * @param array $params Array with scaler params * * @return MediaTransformError Error object if error occurred, false (=no error) otherwise @@ -468,39 +287,17 @@ class BitmapHandler extends ImageHandler { if ( $retval !== 0 ) { $this->logErrorForExternalProcess( $retval, $err, $cmd ); + return $this->getMediaTransformError( $params, $err ); } - return false; # No error - } - /** - * Log an error that occurred in an external process - * - * @param $retval int - * @param $err int - * @param $cmd string - */ - protected function logErrorForExternalProcess( $retval, $err, $cmd ) { - wfDebugLog( 'thumbnail', - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', - wfHostname(), $retval, trim( $err ), $cmd ) ); - } - /** - * Get a MediaTransformError with error 'thumbnail_error' - * - * @param array $params Parameter array as passed to the transform* functions - * @param string $errMsg Error message - * @return MediaTransformError - */ - public function getMediaTransformError( $params, $errMsg ) { - return new MediaTransformError( 'thumbnail_error', $params['clientWidth'], - $params['clientHeight'], $errMsg ); + return false; # No error } /** * Transform an image using the built in GD library * - * @param $image File File associated with this thumbnail + * @param File $image File associated with this thumbnail * @param array $params Array with scaler params * * @return MediaTransformError Error object if error occurred, false (=no error) otherwise @@ -512,24 +309,28 @@ class BitmapHandler extends ImageHandler { # input routine for this. $typemap = array( - 'image/gif' => array( 'imagecreatefromgif', 'palette', 'imagegif' ), - 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', array( __CLASS__, 'imageJpegWrapper' ) ), - 'image/png' => array( 'imagecreatefrompng', 'bits', 'imagepng' ), - 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', 'imagewbmp' ), - 'image/xbm' => array( 'imagecreatefromxbm', 'palette', 'imagexbm' ), + 'image/gif' => array( 'imagecreatefromgif', 'palette', false, 'imagegif' ), + 'image/jpeg' => array( 'imagecreatefromjpeg', 'truecolor', true, + array( __CLASS__, 'imageJpegWrapper' ) ), + 'image/png' => array( 'imagecreatefrompng', 'bits', false, 'imagepng' ), + 'image/vnd.wap.wbmp' => array( 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ), + 'image/xbm' => array( 'imagecreatefromxbm', 'palette', false, 'imagexbm' ), ); + if ( !isset( $typemap[$params['mimeType']] ) ) { $err = 'Image type not supported'; wfDebug( "$err\n" ); $errMsg = wfMessage( 'thumbnail_image-type' )->text(); + return $this->getMediaTransformError( $params, $errMsg ); } - list( $loader, $colorStyle, $saveType ) = $typemap[$params['mimeType']]; + list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']]; if ( !function_exists( $loader ) ) { $err = "Incomplete GD library configuration: missing function $loader"; wfDebug( "$err\n" ); $errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text(); + return $this->getMediaTransformError( $params, $errMsg ); } @@ -537,6 +338,7 @@ class BitmapHandler extends ImageHandler { $err = "File seems to be missing: {$params['srcPath']}"; wfDebug( "$err\n" ); $errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text(); + return $this->getMediaTransformError( $params, $errMsg ); } @@ -574,7 +376,12 @@ class BitmapHandler extends ImageHandler { imagesavealpha( $dst_image, true ); - call_user_func( $saveType, $dst_image, $params['dstPath'] ); + $funcParams = array( $dst_image, $params['dstPath'] ); + if ( $useQuality && isset( $params['quality'] ) ) { + $funcParams[] = $params['quality']; + } + call_user_func_array( $saveType, $funcParams ); + imagedestroy( $dst_image ); imagedestroy( $src_image ); @@ -582,135 +389,21 @@ class BitmapHandler extends ImageHandler { } /** - * Escape a string for ImageMagick's property input (e.g. -set -comment) - * See InterpretImageProperties() in magick/property.c - * @return mixed|string - */ - function escapeMagickProperty( $s ) { - // Double the backslashes - $s = str_replace( '\\', '\\\\', $s ); - // Double the percents - $s = str_replace( '%', '%%', $s ); - // Escape initial - or @ - if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) { - $s = '\\' . $s; - } - return $s; - } - - /** - * Escape a string for ImageMagick's input filenames. See ExpandFilenames() - * and GetPathComponent() in magick/utility.c. - * - * This won't work with an initial ~ or @, so input files should be prefixed - * with the directory name. - * - * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but - * it's broken in a way that doesn't involve trying to convert every file - * in a directory, so we're better off escaping and waiting for the bugfix - * to filter down to users. - * - * @param string $path The file path - * @param bool|string $scene The scene specification, or false if there is none - * @throws MWException - * @return string - */ - function escapeMagickInput( $path, $scene = false ) { - # Die on initial metacharacters (caller should prepend path) - $firstChar = substr( $path, 0, 1 ); - if ( $firstChar === '~' || $firstChar === '@' ) { - throw new MWException( __METHOD__ . ': cannot escape this path name' ); - } - - # Escape glob chars - $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path ); - - return $this->escapeMagickPath( $path, $scene ); - } - - /** - * Escape a string for ImageMagick's output filename. See - * InterpretImageFilename() in magick/image.c. - * @return string - */ - function escapeMagickOutput( $path, $scene = false ) { - $path = str_replace( '%', '%%', $path ); - return $this->escapeMagickPath( $path, $scene ); - } - - /** - * Armour a string against ImageMagick's GetPathComponent(). This is a - * helper function for escapeMagickInput() and escapeMagickOutput(). - * - * @param string $path The file path - * @param bool|string $scene The scene specification, or false if there is none - * @throws MWException - * @return string - */ - protected function escapeMagickPath( $path, $scene = false ) { - # Die on format specifiers (other than drive letters). The regex is - # meant to match all the formats you get from "convert -list format" - if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) { - if ( wfIsWindows() && is_dir( $m[0] ) ) { - // OK, it's a drive letter - // ImageMagick has a similar exception, see IsMagickConflict() - } else { - throw new MWException( __METHOD__ . ': unexpected colon character in path name' ); - } - } - - # If there are square brackets, add a do-nothing scene specification - # to force a literal interpretation - if ( $scene === false ) { - if ( strpos( $path, '[' ) !== false ) { - $path .= '[0--1]'; - } - } else { - $path .= "[$scene]"; - } - return $path; - } - - /** - * Retrieve the version of the installed ImageMagick - * You can use PHPs version_compare() to use this value - * Value is cached for one hour. - * @return String representing the IM version. + * Callback for transformGd when transforming jpeg images. */ - protected function getMagickVersion() { - global $wgMemc; - - $cache = $wgMemc->get( "imagemagick-version" ); - if ( !$cache ) { - global $wgImageMagickConvertCommand; - $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version'; - wfDebug( __METHOD__ . ": Running convert -version\n" ); - $retval = ''; - $return = wfShellExec( $cmd, $retval ); - $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches ); - if ( $x != 1 ) { - wfDebug( __METHOD__ . ": ImageMagick version check failed\n" ); - return null; - } - $wgMemc->set( "imagemagick-version", $matches[1], 3600 ); - return $matches[1]; - } - return $cache; - } - - static function imageJpegWrapper( $dst_image, $thumbPath ) { + // FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95? + static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) { imageinterlace( $dst_image ); - imagejpeg( $dst_image, $thumbPath, 95 ); + imagejpeg( $dst_image, $thumbPath, $quality ); } - /** * Returns whether the current scaler supports rotation (im and gd do) * * @return bool */ - public static function canRotate() { - $scaler = self::getScalerType( null, false ); + public function canRotate() { + $scaler = $this->getScalerType( null, false ); switch ( $scaler ) { case 'im': # ImageMagick supports autorotation @@ -729,9 +422,24 @@ class BitmapHandler extends ImageHandler { } /** - * @param $file File + * @see $wgEnableAutoRotation + * @return bool Whether auto rotation is enabled + */ + public function autoRotateEnabled() { + global $wgEnableAutoRotation; + + if ( $wgEnableAutoRotation === null ) { + // Only enable auto-rotation when we actually can + return $this->canRotate(); + } + + return $wgEnableAutoRotation; + } + + /** + * @param File $file * @param array $params Rotate parameters. - * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 + * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 * @since 1.21 * @return bool */ @@ -741,7 +449,7 @@ class BitmapHandler extends ImageHandler { $rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360; $scene = false; - $scaler = self::getScalerType( null, false ); + $scaler = $this->getScalerType( null, false ); switch ( $scaler ) { case 'im': $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . @@ -751,12 +459,14 @@ class BitmapHandler extends ImageHandler { wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" ); wfProfileIn( 'convert' ); $retval = 0; - $err = wfShellExecWithStderr( $cmd, $retval, $env ); + $err = wfShellExecWithStderr( $cmd, $retval ); wfProfileOut( 'convert' ); if ( $retval !== 0 ) { $this->logErrorForExternalProcess( $retval, $err, $cmd ); + return new MediaTransformError( 'thumbnail_error', 0, 0, $err ); } + return false; case 'imext': $im = new Imagick(); @@ -770,21 +480,11 @@ class BitmapHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', 0, 0, "Unable to write image to {$params['dstPath']}" ); } + return false; default: return new MediaTransformError( 'thumbnail_error', 0, 0, "$scaler rotation not implemented" ); } } - - /** - * Rerurns whether the file needs to be rendered. Returns true if the - * file requires rotation and we are able to rotate it. - * - * @param $file File - * @return bool - */ - public function mustRender( $file ) { - return self::canRotate() && $this->getRotation( $file ) != 0; - } } diff --git a/includes/media/BitmapMetadataHandler.php b/includes/media/BitmapMetadataHandler.php index 7c39c814..dd41c388 100644 --- a/includes/media/BitmapMetadataHandler.php +++ b/includes/media/BitmapMetadataHandler.php @@ -28,12 +28,14 @@ * This sort of acts as an intermediary between MediaHandler::getMetadata * and the various metadata extractors. * - * @todo other image formats. + * @todo Other image formats. * @ingroup Media */ class BitmapMetadataHandler { - + /** @var array */ private $metadata = array(); + + /** @var array Metadata priority */ private $metaPriority = array( 20 => array( 'other' ), 40 => array( 'native' ), @@ -44,6 +46,8 @@ class BitmapMetadataHandler { 100 => array( 'iptc-bad-hash' ), 120 => array( 'exif' ), ); + + /** @var string */ private $iptcType = 'iptc-no-hash'; /** @@ -76,8 +80,8 @@ class BitmapMetadataHandler { * * Parameters are passed to the Exif class. * - * @param $filename string - * @param $byteOrder string + * @param string $filename + * @param string $byteOrder */ function getExif( $filename, $byteOrder ) { global $wgShowEXIF; @@ -89,11 +93,12 @@ class BitmapMetadataHandler { } } } + /** 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 + * @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] ) ) { @@ -111,12 +116,12 @@ class BitmapMetadataHandler { * * This function is generally called by the media handlers' getMetadata() * - * @return Array metadata array + * @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(); + $temp = array(); krsort( $this->metaPriority ); foreach ( $this->metaPriority as $pri ) { foreach ( $pri as $type ) { @@ -138,14 +143,15 @@ class BitmapMetadataHandler { } } } + return $temp; } /** Main entry point for jpeg's. * - * @param string $filename filename (with full path) - * @return array metadata result array. - * @throws MWException on invalid file. + * @param string $filename Filename (with full path) + * @return array Metadata result array. + * @throws MWException On invalid file. */ static function Jpeg( $filename ) { $showXMP = function_exists( 'xml_parser_create_ns' ); @@ -153,7 +159,7 @@ class BitmapMetadataHandler { $seg = JpegMetadataExtractor::segmentSplitter( $filename ); if ( isset( $seg['COM'] ) && isset( $seg['COM'][0] ) ) { - $meta->addMetadata( Array( 'JPEGFileComment' => $seg['COM'] ), 'native' ); + $meta->addMetadata( array( 'JPEGFileComment' => $seg['COM'] ), 'native' ); } if ( isset( $seg['PSIR'] ) && count( $seg['PSIR'] ) > 0 ) { foreach ( $seg['PSIR'] as $curPSIRValue ) { @@ -168,7 +174,6 @@ class BitmapMetadataHandler { * is not well tested and a bit fragile. */ $xmp->parseExtended( $xmpExt ); - } $res = $xmp->getResults(); foreach ( $res as $type => $array ) { @@ -178,6 +183,7 @@ class BitmapMetadataHandler { if ( isset( $seg['byteOrder'] ) ) { $meta->getExif( $filename, $seg['byteOrder'] ); } + return $meta->getMetadataArray(); } @@ -186,15 +192,17 @@ class BitmapMetadataHandler { * merge the png various tEXt chunks to that * are interesting, but for now it only does XMP * - * @param string $filename full path to file - * @return Array Array for storage in img_metadata. + * @param string $filename Full path to file + * @return array Array for storage in img_metadata. */ public static 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 ) { + 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(); @@ -207,6 +215,7 @@ class BitmapMetadataHandler { unset( $array['text'] ); $array['metadata'] = $meta->getMetadataArray(); $array['metadata']['_MW_PNG_VERSION'] = PNGMetadataExtractor::VERSION; + return $array; } @@ -215,8 +224,8 @@ class BitmapMetadataHandler { * They don't really have native metadata, so just merges together * XMP and image comment. * - * @param string $filename full path to file - * @return Array metadata array + * @param string $filename Full path to file + * @return array Metadata array */ public static function GIF( $filename ) { @@ -234,7 +243,6 @@ class BitmapMetadataHandler { foreach ( $xmpRes as $type => $xmpSection ) { $meta->addMetadata( $xmpSection, $type ); } - } unset( $baseArray['comment'] ); @@ -242,6 +250,7 @@ class BitmapMetadataHandler { $baseArray['metadata'] = $meta->getMetadataArray(); $baseArray['metadata']['_MW_GIF_VERSION'] = GIFMetadataExtractor::VERSION; + return $baseArray; } @@ -251,13 +260,12 @@ class BitmapMetadataHandler { * but needs some further processing because PHP's exif support * is stupid...) * - * @todo Add XMP support, so this function actually makes - * sense to put here. + * @todo Add XMP support, so this function actually makes sense to put here. * * The various exceptions this throws are caught later. - * @param $filename String + * @param string $filename * @throws MWException - * @return Array The metadata. + * @return array The metadata. */ public static function Tiff( $filename ) { if ( file_exists( $filename ) ) { @@ -269,6 +277,7 @@ class BitmapMetadataHandler { $data = $exif->getFilteredData(); if ( $data ) { $data['MEDIAWIKI_EXIF_VERSION'] = Exif::version(); + return $data; } else { throw new MWException( "Could not extract data from tiff file $filename" ); @@ -277,12 +286,13 @@ class BitmapMetadataHandler { throw new MWException( "File doesn't exist - $filename" ); } } + /** * Read the first 2 bytes of a tiff file to figure out * Little Endian or Big Endian. Needed for exif stuff. * * @param string $filename The filename - * @return String 'BE' or 'LE' or false + * @return string 'BE' or 'LE' or false */ static function getTiffByteOrder( $filename ) { $fh = fopen( $filename, 'rb' ); diff --git a/includes/media/Bitmap_ClientOnly.php b/includes/media/Bitmap_ClientOnly.php index 63af2552..b91fb8aa 100644 --- a/includes/media/Bitmap_ClientOnly.php +++ b/includes/media/Bitmap_ClientOnly.php @@ -29,11 +29,13 @@ * * @ingroup Media */ +// @codingStandardsIgnoreStart Squiz.Classes.ValidClassName.NotCamelCaps class BitmapHandler_ClientOnly extends BitmapHandler { + // @codingStandardsIgnoreEnd /** - * @param $image File - * @param $params + * @param File $image + * @param array $params * @return bool */ function normaliseParams( $image, &$params ) { @@ -41,10 +43,10 @@ class BitmapHandler_ClientOnly extends BitmapHandler { } /** - * @param $image File - * @param $dstPath - * @param $dstUrl - * @param $params + * @param File $image + * @param string $dstPath + * @param string $dstUrl + * @param array $params * @param int $flags * @return ThumbnailImage|TransformParameterError */ @@ -52,6 +54,7 @@ class BitmapHandler_ClientOnly extends BitmapHandler { if ( !$this->normaliseParams( $image, $params ) ) { return new TransformParameterError( $params ); } + return new ThumbnailImage( $image, $image->getURL(), $image->getLocalRefPath(), $params ); } } diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 9b8116e9..daeb475f 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -27,7 +27,6 @@ * @ingroup Media */ class DjVuHandler extends ImageHandler { - /** * @return bool */ @@ -35,6 +34,7 @@ class DjVuHandler extends ImageHandler { global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML; if ( !$wgDjvuRenderer || ( !$wgDjvuDump && !$wgDjvuToXML ) ) { wfDebug( "DjVu is disabled, please set \$wgDjvuRenderer and \$wgDjvuDump\n" ); + return false; } else { return true; @@ -42,7 +42,7 @@ class DjVuHandler extends ImageHandler { } /** - * @param $file + * @param File $file * @return bool */ function mustRender( $file ) { @@ -50,7 +50,7 @@ class DjVuHandler extends ImageHandler { } /** - * @param $file + * @param File $file * @return bool */ function isMultiPage( $file ) { @@ -68,11 +68,16 @@ class DjVuHandler extends ImageHandler { } /** - * @param $name - * @param $value + * @param string $name + * @param mixed $value * @return bool */ function validateParam( $name, $value ) { + if ( $name === 'page' && trim( $value ) !== (string)intval( $value ) ) { + // Extra junk on the end of page, probably actually a caption + // e.g. [[File:Foo.djvu|thumb|Page 3 of the document shows foo]] + return false; + } if ( in_array( $name, array( 'width', 'height', 'page' ) ) ) { if ( $value <= 0 ) { return false; @@ -85,7 +90,7 @@ class DjVuHandler extends ImageHandler { } /** - * @param $params + * @param array $params * @return bool|string */ function makeParamString( $params ) { @@ -93,11 +98,12 @@ class DjVuHandler extends ImageHandler { if ( !isset( $params['width'] ) ) { return false; } + return "page{$page}-{$params['width']}px"; } /** - * @param $str + * @param string $str * @return array|bool */ function parseParamString( $str ) { @@ -110,7 +116,7 @@ class DjVuHandler extends ImageHandler { } /** - * @param $params + * @param array $params * @return array */ function getScriptParams( $params ) { @@ -121,10 +127,10 @@ class DjVuHandler extends ImageHandler { } /** - * @param $image File - * @param $dstPath - * @param $dstUrl - * @param $params + * @param File $image + * @param string $dstPath + * @param string $dstUrl + * @param array $params * @param int $flags * @return MediaTransformError|ThumbnailImage|TransformParameterError */ @@ -137,6 +143,7 @@ class DjVuHandler extends ImageHandler { if ( !$xml ) { $width = isset( $params['width'] ) ? $params['width'] : 0; $height = isset( $params['height'] ) ? $params['height'] : 0; + return new MediaTransformError( 'thumbnail_error', $width, $height, wfMessage( 'djvu_no_xml' )->text() ); } @@ -162,6 +169,7 @@ class DjVuHandler extends ImageHandler { 'height' => $height, 'page' => $page ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } @@ -174,7 +182,33 @@ class DjVuHandler extends ImageHandler { ); } - $srcPath = $image->getLocalRefPath(); + // Get local copy source for shell scripts + // Thumbnail extraction is very inefficient for large files. + // Provide a way to pool count limit the number of downloaders. + if ( $image->getSize() >= 1e7 ) { // 10MB + $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $image->getName() ), + array( + 'doWork' => function () use ( $image ) { + return $image->getLocalRefPath(); + } + ) + ); + $srcPath = $work->execute(); + } else { + $srcPath = $image->getLocalRefPath(); + } + + if ( $srcPath === false ) { // Failed to get local copy + wfDebugLog( 'thumbnail', + sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"', + wfHostname(), $image->getName() ) ); + + return new MediaTransformError( 'thumbnail_error', + $params['width'], $params['height'], + wfMessage( 'filemissing' )->text() + ); + } + # Use a subshell (brackets) to aggregate stderr from both pipeline commands # before redirecting it to the overall stdout. This works in both Linux and Windows XP. $cmd = '(' . wfEscapeShellArg( @@ -195,9 +229,7 @@ class DjVuHandler extends ImageHandler { $removed = $this->removeBadFile( $dstPath, $retval ); if ( $retval != 0 || $removed ) { - wfDebugLog( 'thumbnail', - sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', - wfHostname(), $retval, trim( $err ), $cmd ) ); + $this->logErrorForExternalProcess( $retval, $err, $cmd ); return new MediaTransformError( 'thumbnail_error', $width, $height, $err ); } else { $params = array( @@ -205,6 +237,7 @@ class DjVuHandler extends ImageHandler { 'height' => $height, 'page' => $page ); + return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } } @@ -212,6 +245,8 @@ class DjVuHandler extends ImageHandler { /** * Cache an instance of DjVuImage in an Image object, return that instance * + * @param File $image + * @param string $path * @return DjVuImage */ function getDjVuImage( $image, $path ) { @@ -222,23 +257,59 @@ class DjVuHandler extends ImageHandler { } else { $deja = $image->dejaImage; } + return $deja; } + /** + * Get metadata, unserializing it if neccessary. + * + * @param File $file The DjVu file in question + * @return string XML metadata as a string. + */ + private function getUnserializedMetadata( File $file ) { + $metadata = $file->getMetadata(); + if ( substr( $metadata, 0, 3 ) === 'dejaMetaTree ) ) { + if ( $gettext && isset( $image->djvuTextTree ) ) { + return $image->djvuTextTree; + } + if ( !$gettext && isset( $image->dejaMetaTree ) ) { return $image->dejaMetaTree; } - $metadata = $image->getMetadata(); + $metadata = $this->getUnserializedMetadata( $image ); if ( !$this->isMetadataValid( $image, $metadata ) ) { wfDebug( "DjVu XML metadata is invalid or missing, should have been fixed in upgradeRow\n" ); + return false; } wfProfileIn( __METHOD__ ); @@ -250,8 +321,11 @@ class DjVuHandler extends ImageHandler { $image->djvuTextTree = false; $tree = new SimpleXMLElement( $metadata ); if ( $tree->getName() == 'mw-djvu' ) { + /** @var SimpleXMLElement $b */ foreach ( $tree->children() as $b ) { if ( $b->getName() == 'DjVuTxt' ) { + // @todo File::djvuTextTree and File::dejaMetaTree are declared + // dynamically. Add a public File::$data to facilitate this? $image->djvuTextTree = $b; } elseif ( $b->getName() == 'DjVuXML' ) { $image->dejaMetaTree = $b; @@ -272,6 +346,11 @@ class DjVuHandler extends ImageHandler { } } + /** + * @param File $image + * @param string $path + * @return bool|array False on failure + */ function getImageSize( $image, $path ) { return $this->getDjVuImage( $image, $path )->getImageSize(); } @@ -283,12 +362,20 @@ class DjVuHandler extends ImageHandler { $magic = MimeMagic::singleton(); $mime = $magic->guessTypesForExtension( $wgDjvuOutputExtension ); } + return array( $wgDjvuOutputExtension, $mime ); } function getMetadata( $image, $path ) { wfDebug( "Getting DjVu metadata for $path\n" ); - return $this->getDjVuImage( $image, $path )->retrieveMetaData(); + + $xml = $this->getDjVuImage( $image, $path )->retrieveMetaData(); + if ( $xml === false ) { + // Special value so that we don't repetitively try and decode a broken file. + return serialize( array( 'error' => 'Error extracting metadata' ) ); + } else { + return serialize( array( 'xml' => $xml ) ); + } } function getMetadataType( $image ) { @@ -304,6 +391,7 @@ class DjVuHandler extends ImageHandler { if ( !$tree ) { return false; } + return count( $tree->xpath( '//OBJECT' ) ); } @@ -324,6 +412,11 @@ class DjVuHandler extends ImageHandler { } } + /** + * @param File $image + * @param int $page Page number to get information for + * @return bool|string Page text or false when no text found. + */ function getPageText( $image, $page ) { $tree = $this->getMetaTree( $image, true ); if ( !$tree ) { @@ -333,11 +426,10 @@ class DjVuHandler extends ImageHandler { $o = $tree->BODY[0]->PAGE[$page - 1]; if ( $o ) { $txt = $o['value']; + return $txt; } else { return false; } - } - } diff --git a/includes/media/DjVuImage.php b/includes/media/DjVuImage.php index 54efe7a8..6ff19c90 100644 --- a/includes/media/DjVuImage.php +++ b/includes/media/DjVuImage.php @@ -3,7 +3,7 @@ * DjVu image handler. * * Copyright © 2006 Brion Vibber - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * 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 @@ -34,6 +34,11 @@ * @ingroup Media */ class DjVuImage { + /** + * @const DJVUTXT_MEMORY_LIMIT Memory limit for the DjVu description software + */ + const DJVUTXT_MEMORY_LIMIT = 300000; + /** * Constructor * @@ -43,23 +48,19 @@ class DjVuImage { $this->mFilename = $filename; } - /** - * @const DJVUTXT_MEMORY_LIMIT Memory limit for the DjVu description software - */ - const DJVUTXT_MEMORY_LIMIT = 300000; - /** * Check if the given file is indeed a valid DjVu image file * @return bool */ public function isValid() { $info = $this->getInfo(); + return $info !== false; } /** * Return data in the style of getimagesize() - * @return array or false on failure + * @return array|bool Array or false on failure */ public function getImageSize() { $data = $this->getInfo(); @@ -71,6 +72,7 @@ class DjVuImage { return array( $width, $height, 'DjVu', "width=\"$width\" height=\"$height\"" ); } + return false; } @@ -82,8 +84,11 @@ class DjVuImage { function dump() { $file = fopen( $this->mFilename, 'rb' ); $header = fread( $file, 12 ); - // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with + // something that explicitly initializes local variables. extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); + /** @var string $chunk + * @var string $chunkLength */ echo "$chunk $chunkLength\n"; $this->dumpForm( $file, $chunkLength, 1 ); fclose( $file ); @@ -98,8 +103,11 @@ class DjVuImage { if ( $chunkHeader == '' ) { break; } - // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with + // something that explicitly initializes local variables. extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); + /** @var string $chunk + * @var string $chunkLength */ echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; if ( $chunk == 'FORM' ) { @@ -120,6 +128,7 @@ class DjVuImage { wfRestoreWarnings(); if ( $file === false ) { wfDebug( __METHOD__ . ": missing or failed file read\n" ); + return false; } @@ -129,9 +138,14 @@ class DjVuImage { if ( strlen( $header ) < 16 ) { wfDebug( __METHOD__ . ": too short file header\n" ); } else { - // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with + // something that explicitly initializes local variables. extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); + /** @var string $magic + * @var string $subtype + * @var string $formLength + * @var string $formType */ if ( $magic != 'AT&T' ) { wfDebug( __METHOD__ . ": not a DjVu file\n" ); } elseif ( $subtype == 'DJVU' ) { @@ -145,6 +159,7 @@ class DjVuImage { } } fclose( $file ); + return $info; } @@ -153,8 +168,12 @@ class DjVuImage { if ( strlen( $header ) < 8 ) { return array( false, 0 ); } else { - // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with + // something that explicitly initializes local variables. extract( unpack( 'a4chunk/Nlength', $header ) ); + + /** @var string $chunk + * @var string $length */ return array( $chunk, $length ); } } @@ -182,6 +201,7 @@ class DjVuImage { $subtype = fread( $file, 4 ); if ( $subtype == 'DJVU' ) { wfDebug( __METHOD__ . ": found first subpage\n" ); + return $this->getPageInfo( $file, $length ); } $this->skipChunk( $file, $length - 4 ); @@ -192,6 +212,7 @@ class DjVuImage { } while ( $length != 0 && !feof( $file ) && ftell( $file ) - $start < $formLength ); wfDebug( __METHOD__ . ": multi-page DJVU file contained no pages\n" ); + return false; } @@ -199,20 +220,24 @@ class DjVuImage { list( $chunk, $length ) = $this->readChunk( $file ); if ( $chunk != 'INFO' ) { wfDebug( __METHOD__ . ": expected INFO chunk, got '$chunk'\n" ); + return false; } if ( $length < 9 ) { wfDebug( __METHOD__ . ": INFO should be 9 or 10 bytes, found $length\n" ); + return false; } $data = fread( $file, $length ); if ( strlen( $data ) < $length ) { wfDebug( __METHOD__ . ": INFO chunk cut off\n" ); + return false; } - // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with + // something that explicitly initializes local variables. extract( unpack( 'nwidth/' . 'nheight/' . @@ -220,8 +245,16 @@ class DjVuImage { 'Cmajor/' . 'vresolution/' . 'Cgamma', $data ) ); + # Newer files have rotation info in byte 10, but we don't use it yet. + /** @var string $width + * @var string $height + * @var string $major + * @var string $minor + * @var string $resolution + * @var string $length + * @var string $gamma */ return array( 'width' => $width, 'height' => $height, @@ -284,17 +317,21 @@ EOR; } } wfProfileOut( __METHOD__ ); + return $xml; } function pageTextCallback( $matches ) { # Get rid of invalid UTF-8, strip control characters - return ''; + $val = htmlspecialchars( UtfNormal::cleanUp( stripcslashes( $matches[1] ) ) ); + $val = str_replace( array( "\n", '�' ), array( ' ', '' ), $val ); + return ''; } /** * Hack to temporarily work around djvutoxml bug - * @return bool|string + * @param string $dump + * @return string */ function convertDumpToXML( $dump ) { if ( strval( $dump ) == '' ) { @@ -334,6 +371,7 @@ EOT; if ( preg_match( '/^ *DIRM.*indirect/', $line ) ) { wfDebug( "Indirect multi-page DjVu document, bad for server!\n" ); + return false; } if ( preg_match( '/^ *FORM:DJVU/', $line ) ) { @@ -352,6 +390,7 @@ EOT; } $xml .= "\n\n"; + return $xml; } @@ -367,8 +406,13 @@ EOT; break; } - if ( preg_match( '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', $line, $m ) ) { - $xml .= Xml::tags( 'OBJECT', + if ( preg_match( + '/^ *INFO *\[\d*\] *DjVu *(\d+)x(\d+), *\w*, *(\d+) *dpi, *gamma=([0-9.-]+)/', + $line, + $m + ) ) { + $xml .= Xml::tags( + 'OBJECT', array( #'data' => '', #'type' => 'image/x.djvu', @@ -377,13 +421,15 @@ EOT; #'usemap' => '', ), "\n" . - Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" . - Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n" + Xml::element( 'PARAM', array( 'name' => 'DPI', 'value' => $m[3] ) ) . "\n" . + Xml::element( 'PARAM', array( 'name' => 'GAMMA', 'value' => $m[4] ) ) . "\n" ) . "\n"; + return true; } $line = strtok( "\n" ); } + # Not found return false; } diff --git a/includes/media/Exif.php b/includes/media/Exif.php index 9a2794a5..018b58c5 100644 --- a/includes/media/Exif.php +++ b/includes/media/Exif.php @@ -30,87 +30,82 @@ * @ingroup Media */ class Exif { + /** An 8-bit (1-byte) unsigned integer. */ + const BYTE = 1; - 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 SHORT_OR_LONG = 6; //!< A 16-bit (2-byte) or 32-bit (4-byte) unsigned integer. - 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 + /** An 8-bit byte containing one 7-bit ASCII code. + * The final byte is terminated with NULL. */ + const ASCII = 2; - /** - * 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 16-bit (2-byte) unsigned integer. */ + const SHORT = 3; - /** - * The raw Exif data returned by exif_read_data() - */ - var $mRawExifData; + /** A 32-bit (4-byte) unsigned integer. */ + const LONG = 4; - /** - * 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 + /** Two LONGs. The first LONG is the numerator and the second LONG expresses + * the denominator */ - var $mFilteredExifData; + const RATIONAL = 5; - /** - * Filtered and formatted Exif data, see FormatMetadata::getFormattedData() - */ - var $mFormattedExifData; + /** A 16-bit (2-byte) or 32-bit (4-byte) unsigned integer. */ + const SHORT_OR_LONG = 6; - //@} + /** An 8-bit byte that can take any value depending on the field definition */ + const UNDEFINED = 7; - //@{ - /* @var string - * @private - */ + /** A 32-bit (4-byte) signed integer (2's complement notation), */ + const SLONG = 9; - /** - * The file being processed + /** Two SLONGs. The first SLONG is the numerator and the second SLONG is + * the denominator. */ - var $file; + const SRATIONAL = 10; - /** - * The basename of the file being processed + /** A fake value for things we don't want or don't support. */ + const IGNORE = -1; + + /** @var array 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 $basename; + private $mExifTags; - /** - * The private log to log to, e.g. 'exif' + /** @var array The raw Exif data returned by exif_read_data() */ + private $mRawExifData; + + /** @var array 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 $log = false; + private $mFilteredExifData; - /** - * The byte order of the file. Needed because php's - * extension doesn't fully process some obscure props. + /** @var string The file being processed */ + private $file; + + /** @var string The basename of the file being processed */ + private $basename; + + /** @var string The private log to log to, e.g. 'exif' */ + private $log = false; + + /** @var string The byte order of the file. Needed because php's extension + * doesn't fully process some obscure props. */ private $byteOrder; - //@} /** * Constructor * - * @param string $file filename. - * @param string $byteOrder Type of byte ordering either 'BE' (Big Endian) or 'LE' (Little Endian). Default ''. + * @param string $file Filename. + * @param string $byteOrder Type of byte ordering either 'BE' (Big Endian) + * or 'LE' (Little Endian). Default ''. * @throws MWException * @todo 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. + * 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, $byteOrder = '' ) { /** @@ -125,122 +120,123 @@ class Exif { # TIFF Rev. 6.0 Attribute Information (p22) 'IFD0' => array( # Tags relating to image structure - 'ImageWidth' => Exif::SHORT_OR_LONG, # Image width - 'ImageLength' => Exif::SHORT_OR_LONG, # Image height - 'BitsPerSample' => array( Exif::SHORT, 3 ), # Number of bits per component + 'ImageWidth' => Exif::SHORT_OR_LONG, # Image width + 'ImageLength' => Exif::SHORT_OR_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) + '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_OR_LONG, # Image data location - 'RowsPerStrip' => Exif::SHORT_OR_LONG, # Number of rows per strip - 'StripByteCounts' => Exif::SHORT_OR_LONG, # Bytes per compressed strip - 'JPEGInterchangeFormat' => Exif::SHORT_OR_LONG, # Offset to JPEG SOI - 'JPEGInterchangeFormatLength' => Exif::SHORT_OR_LONG, # Bytes of JPEG data + 'StripOffsets' => Exif::SHORT_OR_LONG, # Image data location + 'RowsPerStrip' => Exif::SHORT_OR_LONG, # Number of rows per strip + 'StripByteCounts' => Exif::SHORT_OR_LONG, # Bytes per compressed strip + 'JPEGInterchangeFormat' => Exif::SHORT_OR_LONG, # Offset to JPEG SOI + 'JPEGInterchangeFormatLength' => Exif::SHORT_OR_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 + 'TransferFunction' => Exif::IGNORE, # Transfer function + 'WhitePoint' => array( Exif::RATIONAL, 2 ), # White point chromaticity + 'PrimaryChromaticities' => array( Exif::RATIONAL, 6 ), # Chromaticities of primarities + # Color space transformation matrix coefficients #p27 + 'YCbCrCoefficients' => array( Exif::RATIONAL, 3 ), + '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 + '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 + # @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 + '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 + '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_OR_LONG, # Valid image width - 'PixelXDimension' => Exif::SHORT_OR_LONG, # Valid image height + 'ComponentsConfiguration' => Exif::UNDEFINED, # Meaning of each component #p33 + 'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode + 'PixelYDimension' => Exif::SHORT_OR_LONG, # Valid image width + 'PixelXDimension' => Exif::SHORT_OR_LONG, # Valid image height # Tags relating to related user information - 'MakerNote' => Exif::IGNORE, # Manufacturer notes - 'UserComment' => Exif::UNDEFINED, # User comments #p34 + 'MakerNote' => Exif::IGNORE, # Manufacturer notes + 'UserComment' => Exif::UNDEFINED, # User comments #p34 # Tags relating to related file information - 'RelatedSoundFile' => Exif::ASCII, # Related audio file + '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 + '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 + '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 + '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 + 'SubjectDistanceRange' => Exif::SHORT, # Subject distance range #p51 - 'ImageUniqueID' => Exif::ASCII, # Unique image ID + 'ImageUniqueID' => Exif::ASCII, # Unique image ID ), # GPS Attribute Information (p52) @@ -248,38 +244,38 @@ class Exif { '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 + '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 + '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 ), ); @@ -302,14 +298,15 @@ class Exif { $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." ); + 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->mRawExifData = $data ?: array(); $this->makeFilteredData(); $this->collapseData(); $this->debugFile( __FUNCTION__, false ); @@ -319,16 +316,16 @@ class Exif { * Make $this->mFilteredExifData */ function makeFilteredData() { - $this->mFilteredExifData = Array(); + $this->mFilteredExifData = array(); foreach ( array_keys( $this->mRawExifData ) as $section ) { - if ( !in_array( $section, array_keys( $this->mExifTags ) ) ) { + if ( !array_key_exists( $section, $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] ) ) ) { + if ( !array_key_exists( $tag, $this->mExifTags[$section] ) ) { $this->debug( $tag, __FUNCTION__, "'$tag' is not a valid tag in '$section'" ); continue; } @@ -371,15 +368,17 @@ class Exif { $this->exifGPStoNumber( 'GPSLongitude' ); $this->exifGPStoNumber( 'GPSDestLongitude' ); - if ( isset( $this->mFilteredExifData['GPSAltitude'] ) && isset( $this->mFilteredExifData['GPSAltitudeRef'] ) ) { - - // We know altitude data is a / from the validation functions ran earlier. - // But multiplying such a string by -1 doesn't work well, so convert. + if ( isset( $this->mFilteredExifData['GPSAltitude'] ) + && isset( $this->mFilteredExifData['GPSAltitudeRef'] ) + ) { + // We know altitude data is a / from the validation + // functions ran earlier. But multiplying such a string by -1 + // doesn't work well, so convert. list( $num, $denom ) = explode( '/', $this->mFilteredExifData['GPSAltitude'] ); $this->mFilteredExifData['GPSAltitude'] = $num / $denom; if ( $this->mFilteredExifData['GPSAltitudeRef'] === "\1" ) { - $this->mFilteredExifData['GPSAltitude'] *= - 1; + $this->mFilteredExifData['GPSAltitude'] *= -1; } unset( $this->mFilteredExifData['GPSAltitudeRef'] ); } @@ -397,7 +396,9 @@ class Exif { if ( isset( $this->mFilteredExifData['ComponentsConfiguration'] ) ) { $val = $this->mFilteredExifData['ComponentsConfiguration']; $ccVals = array(); - for ( $i = 0; $i < strlen( $val ); $i++ ) { + + $strLen = strlen( $val ); + for ( $i = 0; $i < $strLen; $i++ ) { $ccVals[$i] = ord( substr( $val, $i, 1 ) ); } $ccVals['_type'] = 'ol'; //this is for formatting later. @@ -414,12 +415,15 @@ class Exif { if ( isset( $this->mFilteredExifData['GPSVersion'] ) ) { $val = $this->mFilteredExifData['GPSVersion']; $newVal = ''; - for ( $i = 0; $i < strlen( $val ); $i++ ) { + + $strLen = strlen( $val ); + for ( $i = 0; $i < $strLen; $i++ ) { if ( $i !== 0 ) { $newVal .= '.'; } $newVal .= ord( substr( $val, $i, 1 ) ); } + if ( $this->byteOrder === 'LE' ) { // Need to reverse the string $newVal2 = ''; @@ -432,13 +436,13 @@ class Exif { } 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 string $prop prop name. + * @param string $prop Prop name */ private function charCodeString( $prop ) { if ( isset( $this->mFilteredExifData[$prop] ) ) { @@ -448,6 +452,7 @@ class Exif { $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, false ); unset( $this->mFilteredExifData[$prop] ); + return; } $charCode = substr( $this->mFilteredExifData[$prop], 0, 8 ); @@ -465,8 +470,6 @@ class Exif { $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 ) { wfSuppressWarnings(); $val = iconv( $charset, 'UTF-8//IGNORE', $val ); @@ -488,6 +491,7 @@ class Exif { //only whitespace. $this->debug( $this->mFilteredExifData[$prop], __FUNCTION__, "$prop: Is only whitespace" ); unset( $this->mFilteredExifData[$prop] ); + return; } @@ -495,28 +499,32 @@ class Exif { $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 string $prop name of property + * @param string $prop 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) + * @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' ) ) { + 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] ); @@ -525,7 +533,7 @@ class Exif { $res += ( $num / $denom ) * ( 1 / 3600 ); if ( $dir === 'S' || $dir === 'W' ) { - $res *= - 1; // make negative + $res *= -1; // make negative } } @@ -540,17 +548,6 @@ class Exif { } } - /** - * Use FormatMetadata to create formatted values for display to user - * (is this ever used?) - * - * @deprecated since 1.18 - */ - function makeFormattedData() { - wfDeprecated( __METHOD__, '1.18' ); - $this->mFormattedExifData = FormatMetadata::getFormattedData( - $this->mFilteredExifData ); - } /**#@-*/ /**#@+ @@ -566,26 +563,12 @@ class Exif { /** * Get $this->mFilteredExifData + * @return array */ function getFilteredData() { return $this->mFilteredExifData; } - /** - * Get $this->mFormattedExifData - * - * This returns the data for display to user. - * Its unclear if this is ever used. - * - * @deprecated since 1.18 - */ - function getFormattedData() { - wfDeprecated( __METHOD__, '1.18' ); - if ( !$this->mFormattedExifData ) { - $this->makeFormattedData(); - } - return $this->mFormattedExifData; - } /**#@-*/ /** @@ -604,26 +587,26 @@ class Exif { 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 + * @param mixed $in 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; } } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ private function isASCII( $in ) { @@ -633,11 +616,13 @@ class Exif { 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; } @@ -645,93 +630,110 @@ class Exif { } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ 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; } } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ 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; } } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ 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 + + # Avoid division by zero + if ( !is_array( $in ) + && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) + ) { return $this->isLong( $m[1] ) && $this->isLong( $m[2] ); } else { $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); + return false; } } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ private function isUndefined( $in ) { $this->debug( $in, __FUNCTION__, true ); + return true; } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ private function isSlong( $in ) { if ( $this->isLong( abs( $in ) ) ) { $this->debug( $in, __FUNCTION__, true ); + return true; } else { $this->debug( $in, __FUNCTION__, false ); + return false; } } /** - * @param $in + * @param mixed $in The input value to check * @return bool */ 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 + + # Avoid division by zero + if ( !is_array( $in ) && + preg_match( '/^(-?\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) + ) { 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 string $section section where tag is located. - * @param string $tag the tag to check. - * @param $val Mixed: the value of the tag. - * @param $recursive Boolean: true if called recursively for array types. + * @param string $section Section where tag is located. + * @param string $tag The tag to check. + * @param mixed $val The value of the tag. + * @param bool $recursive True if called recursively for array types. * @return bool */ private function validate( $section, $tag, $val, $recursive = false ) { @@ -747,6 +749,7 @@ class Exif { $count = count( $val ); if ( $ecount != $count ) { $this->debug( $val, __FUNCTION__, "Expected $ecount elements for $tag but got $count" ); + return false; } if ( $count > 1 ) { @@ -755,42 +758,54 @@ class Exif { 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::SHORT_OR_LONG: $this->debug( $val, __FUNCTION__, $debug ); + return $this->isShort( $val ) || $this->isLong( $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::IGNORE: $this->debug( $val, __FUNCTION__, $debug ); + return false; default: $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" ); + return false; } } @@ -798,11 +813,9 @@ class Exif { /** * Convenience function for debugging output * - * @private - * - * @param $in Mixed: - * @param $fname String: - * @param $action Mixed: , default NULL. + * @param mixed $in Arrays will be processed with print_r(). + * @param string $fname Function name to log. + * @param string|bool|null $action Default null. */ private function debug( $in, $fname, $action = null ) { if ( !$this->log ) { @@ -815,23 +828,21 @@ class Exif { } if ( $action === true ) { - wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n" ); + wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)" ); } elseif ( $action === false ) { - wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n" ); + wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)" ); } elseif ( $action === null ) { - wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n" ); + wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)" ); } else { - wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n" ); + wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')" ); } } /** * Convenience function for debugging output * - * @private - * - * @param string $fname the name of the function calling this function - * @param $io Boolean: Specify whether we're beginning or ending + * @param string $fname The name of the function calling this function + * @param bool $io Specify whether we're beginning or ending */ private function debugFile( $fname, $io ) { if ( !$this->log ) { @@ -839,9 +850,9 @@ class Exif { } $class = ucfirst( __CLASS__ ); if ( $io ) { - wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); + wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'" ); } else { - wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" ); + wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'" ); } } } diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index d8d0bede..b7657cb3 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -28,7 +28,6 @@ * @ingroup Media */ class ExifBitmapHandler extends BitmapHandler { - const BROKEN_FILE = '-1'; // error extracting metadata const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata. @@ -61,22 +60,30 @@ class ExifBitmapHandler extends BitmapHandler { . $metadata['Software'][0][1] . ')'; } + $formatter = new FormatMetadata; + // ContactInfo also has to be dealt with specially if ( isset( $metadata['Contact'] ) ) { $metadata['Contact'] = - FormatMetadata::collapseContactInfo( + $formatter->collapseContactInfo( $metadata['Contact'] ); } foreach ( $metadata as &$val ) { if ( is_array( $val ) ) { - $val = FormatMetadata::flattenArray( $val, 'ul', $avoidHtml ); + $val = $formatter->flattenArrayReal( $val, 'ul', $avoidHtml ); } } $metadata['MEDIAWIKI_EXIF_VERSION'] = 1; + return $metadata; } + /** + * @param File $image + * @param array $metadata + * @return bool|int + */ function isMetadataValid( $image, $metadata ) { global $wgShowEXIF; if ( !$wgShowEXIF ) { @@ -87,6 +94,7 @@ class ExifBitmapHandler extends BitmapHandler { # 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 ) { @@ -95,47 +103,57 @@ class ExifBitmapHandler extends BitmapHandler { 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 ) - { + 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; } /** - * @param $image File + * @param File $image * @return array|bool */ function formatMetadata( $image ) { - $metadata = $image->getMetadata(); - if ( $metadata === self::OLD_BROKEN_FILE || - $metadata === self::BROKEN_FILE || - $this->isMetadataValid( $image, $metadata ) === self::METADATA_BAD ) - { + $meta = $this->getCommonMetaArray( $image ); + if ( count( $meta ) === 0 ) { + return false; + } + + return $this->formatMetadataHelper( $meta ); + } + + public function getCommonMetaArray( File $file ) { + $metadata = $file->getMetadata(); + if ( $metadata === self::OLD_BROKEN_FILE + || $metadata === self::BROKEN_FILE + || $this->isMetadataValid( $file, $metadata ) === self::METADATA_BAD + ) { // So we don't try and display metadata from PagedTiffHandler // for example when using InstantCommons. - return false; + return array(); } $exif = unserialize( $metadata ); if ( !$exif ) { - return false; + return array(); } unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); - if ( count( $exif ) == 0 ) { - return false; - } - return $this->formatMetadataHelper( $exif ); + + return $exif; } function getMetadataType( $image ) { @@ -151,12 +169,11 @@ class ExifBitmapHandler extends BitmapHandler { * @return array */ function getImageSize( $image, $path ) { - global $wgEnableAutoRotation; $gis = parent::getImageSize( $image, $path ); // Don't just call $image->getMetadata(); FSFile::getPropsFromPath() calls us with a bogus object. // This may mean we read EXIF data twice on initial upload. - if ( $wgEnableAutoRotation ) { + if ( $this->autoRotateEnabled() ) { $meta = $this->getMetadata( $image, $path ); $rotation = $this->getRotationForExif( $meta ); } else { @@ -168,6 +185,7 @@ class ExifBitmapHandler extends BitmapHandler { $gis[0] = $gis[1]; $gis[1] = $width; } + return $gis; } @@ -180,16 +198,16 @@ class ExifBitmapHandler extends BitmapHandler { * the width and height we normally work with is logical, and will match * any produced output views. * - * @param $file File + * @param File $file * @return int 0, 90, 180 or 270 */ public function getRotation( $file ) { - global $wgEnableAutoRotation; - if ( !$wgEnableAutoRotation ) { + if ( !$this->autoRotateEnabled() ) { return 0; } $data = $file->getMetadata(); + return $this->getRotationForExif( $data ); } @@ -199,8 +217,7 @@ class ExifBitmapHandler extends BitmapHandler { * * @param string $data * @return int 0, 90, 180 or 270 - * @todo FIXME orientation can include flipping as well; see if this is an - * issue! + * @todo FIXME: Orientation can include flipping as well; see if this is an issue! */ protected function getRotationForExif( $data ) { if ( !$data ) { @@ -222,6 +239,7 @@ class ExifBitmapHandler extends BitmapHandler { return 0; } } + return 0; } } diff --git a/includes/media/FormatMetadata.php b/includes/media/FormatMetadata.php index 1c5136f5..43569539 100644 --- a/includes/media/FormatMetadata.php +++ b/includes/media/FormatMetadata.php @@ -43,8 +43,26 @@ * is already a large number of messages using the 'exif' prefix. * * @ingroup Media + * @since 1.23 the class extends ContextSource and various formerly-public + * internal methods are private */ -class FormatMetadata { +class FormatMetadata extends ContextSource { + /** + * Only output a single language for multi-language fields + * @var bool + * @since 1.23 + */ + protected $singleLang = false; + + /** + * Trigger only outputting single language for multilanguage fields + * + * @param bool $val + * @since 1.23 + */ + public function setSingleLanguage( $val ) { + $this->singleLang = $val; + } /** * Numbers given by Exif user agents are often magical, that is they @@ -52,13 +70,34 @@ class FormatMetadata { * value which most of the time are plain integers. This function * formats Exif (and other metadata) values into human readable form. * - * @param array $tags the Exif data to format ( as returned by - * Exif::getFilteredData() or BitmapMetadataHandler ) + * This is the usual entry point for this class. + * + * @param array $tags The Exif data to format ( as returned by + * Exif::getFilteredData() or BitmapMetadataHandler ) + * @param bool|IContextSource $context Context to use (optional) * @return array */ - public static function getFormattedData( $tags ) { - global $wgLang; + public static function getFormattedData( $tags, $context = false ) { + $obj = new FormatMetadata; + if ( $context ) { + $obj->setContext( $context ); + } + return $obj->makeFormattedData( $tags ); + } + + /** + * 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 array $tags The Exif data to format ( as returned by + * Exif::getFilteredData() or BitmapMetadataHandler ) + * @return array + * @since 1.23 + */ + public function makeFormattedData( $tags ) { $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; unset( $tags['ResolutionUnit'] ); @@ -67,7 +106,7 @@ class FormatMetadata { // 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 ); + $vals = array( $vals ); } // _type is a special value to say what array type @@ -107,7 +146,7 @@ class FormatMetadata { $time = wfTimestamp( TS_MW, '1971:01:01 ' . $tags[$tag] ); // the 1971:01:01 is just a placeholder, and not shown to user. if ( $time && intval( $time ) > 0 ) { - $tags[$tag] = $wgLang->time( $time ); + $tags[$tag] = $this->getLanguage()->time( $time ); } } catch ( TimestampException $e ) { // This shouldn't happen, but we've seen bad formats @@ -121,727 +160,892 @@ class FormatMetadata { // instead of the other props which are single // valued (mostly) so handle as a special case. if ( $tag === 'Contact' ) { - $vals = self::collapseContactInfo( $vals ); + $vals = $this->collapseContactInfo( $vals ); continue; } foreach ( $vals as &$val ) { switch ( $tag ) { - case 'Compression': - switch ( $val ) { - case 1: case 2: case 3: case 4: - case 5: case 6: case 7: case 8: - case 32773: case 32946: case 34712: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'Compression': + switch ( $val ) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 32773: + case 32946: + case 34712: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'PhotometricInterpretation': - switch ( $val ) { - case 2: case 6: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'PhotometricInterpretation': + switch ( $val ) { + case 2: + case 6: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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: - /* If not recognized, display as is. */ + case 'Orientation': + switch ( $val ) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'PlanarConfiguration': - switch ( $val ) { - case 1: case 2: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ - break; - } - break; - - // TODO: YCbCrSubSampling - case 'YCbCrPositioning': - switch ( $val ) { - case 1: - case 2: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'PlanarConfiguration': + switch ( $val ) { + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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: - /* If not recognized, display as is. */ - 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: - /* If not recognized, display as is. */ + // TODO: YCbCrSubSampling + case 'YCbCrPositioning': + switch ( $val ) { + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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 ); + case 'XResolution': + case 'YResolution': + switch ( $resolutionunit ) { + case 2: + $val = $this->exifMsg( 'XYResolution', 'i', $this->formatNum( $val ) ); + break; + case 3: + $val = $this->exifMsg( 'XYResolution', 'c', $this->formatNum( $val ) ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + // TODO: YCbCrCoefficients #p27 (see annex E) + case 'ExifVersion': + case 'FlashpixVersion': + $val = "$val" / 100; 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 = wfMessage( 'exif-unknowndate' )->text(); - } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', $val ) ) { - // Full date. - $time = wfTimestamp( TS_MW, $val ); - if ( $time && intval( $time ) > 0 ) { - $val = $wgLang->timeanddate( $time ); - } - } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) { - // No second field. Still format the same - // since timeanddate doesn't include seconds anyways, - // but second still available in api - $time = wfTimestamp( TS_MW, $val . ':00' ); - if ( $time && intval( $time ) > 0 ) { - $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 && intval( $time ) > 0 ) { - $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 ); + case 'ColorSpace': + switch ( $val ) { + case 1: + case 65535: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'ComponentsConfiguration': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'SubjectDistance': - $val = self::msg( $tag, '', self::formatNum( $val ) ); - 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 = $this->msg( 'exif-unknowndate' )->text(); + } elseif ( preg_match( + '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/D', + $val + ) ) { + // Full date. + $time = wfTimestamp( TS_MW, $val ); + if ( $time && intval( $time ) > 0 ) { + $val = $this->getLanguage()->timeanddate( $time ); + } + } elseif ( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d)$/D', $val ) ) { + // No second field. Still format the same + // since timeanddate doesn't include seconds anyways, + // but second still available in api + $time = wfTimestamp( TS_MW, $val . ':00' ); + if ( $time && intval( $time ) > 0 ) { + $val = $this->getLanguage()->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 && intval( $time ) > 0 ) { + $val = $this->getLanguage()->date( $time ); + } + } + // else it will just output $val without formatting it. + 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 ); + case 'ExposureProgram': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'SubjectDistance': + $val = $this->exifMsg( $tag, '', $this->formatNum( $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 ); + + case 'MeteringMode': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 255: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + 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 = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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, + + 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, - ); - $flashMsgs = array(); - # 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; + ); + $flashMsgs = array(); + # 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->exifMsg( $fullTag, $subValue ); } - $fullTag = $tag . '-' . $subTag; - $flashMsgs[] = self::msg( $fullTag, $subValue ); - } - $val = $wgLang->commaList( $flashMsgs ); - break; - - case 'FocalPlaneResolutionUnit': - switch ( $val ) { - case 2: - $val = self::msg( $tag, $val ); + $val = $this->getLanguage()->commaList( $flashMsgs ); break; - default: - /* If not recognized, display as is. */ - 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: - /* If not recognized, display as is. */ + case 'FocalPlaneResolutionUnit': + switch ( $val ) { + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'FileSource': - switch ( $val ) { - case 3: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'SensingMethod': + switch ( $val ) { + case 1: + case 2: + case 3: + case 4: + case 5: + case 7: + case 8: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'SceneType': - switch ( $val ) { - case 1: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'FileSource': + switch ( $val ) { + case 3: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'CustomRendered': - switch ( $val ) { - case 0: case 1: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'SceneType': + switch ( $val ) { + case 1: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'ExposureMode': - switch ( $val ) { - case 0: case 1: case 2: - $val = self::msg( $tag, $val ); - break; - default: - /* If not recognized, display as is. */ + case 'CustomRendered': + switch ( $val ) { + case 0: + case 1: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'WhiteBalance': - switch ( $val ) { - case 0: case 1: - $val = self::msg( $tag, $val ); + case 'ExposureMode': + switch ( $val ) { + case 0: + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'WhiteBalance': + switch ( $val ) { + case 0: + case 1: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'SceneCaptureType': - switch ( $val ) { - case 0: case 1: case 2: case 3: - $val = self::msg( $tag, $val ); + case 'SceneCaptureType': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'GainControl': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + case 4: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'GainControl': - switch ( $val ) { - case 0: case 1: case 2: case 3: case 4: - $val = self::msg( $tag, $val ); + case 'Contrast': + switch ( $val ) { + case 0: + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'Saturation': + switch ( $val ) { + case 0: + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'Contrast': - switch ( $val ) { - case 0: case 1: case 2: - $val = self::msg( $tag, $val ); + case 'Sharpness': + switch ( $val ) { + case 0: + case 1: + case 2: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'SubjectDistanceRange': + switch ( $val ) { + case 0: + case 1: + case 2: + case 3: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'Saturation': - switch ( $val ) { - case 0: case 1: case 2: - $val = self::msg( $tag, $val ); + //The GPS...Ref values are kept for compatibility, probably won't be reached. + case 'GPSLatitudeRef': + case 'GPSDestLatitudeRef': + switch ( $val ) { + case 'N': + case 'S': + $val = $this->exifMsg( 'GPSLatitude', $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'GPSLongitudeRef': + case 'GPSDestLongitudeRef': + switch ( $val ) { + case 'E': + case 'W': + $val = $this->exifMsg( 'GPSLongitude', $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'Sharpness': - switch ( $val ) { - case 0: case 1: case 2: - $val = self::msg( $tag, $val ); + case 'GPSAltitude': + if ( $val < 0 ) { + $val = $this->exifMsg( 'GPSAltitude', 'below-sealevel', $this->formatNum( -$val, 3 ) ); + } else { + $val = $this->exifMsg( 'GPSAltitude', 'above-sealevel', $this->formatNum( $val, 3 ) ); + } break; - default: - /* If not recognized, display as is. */ + + case 'GPSStatus': + switch ( $val ) { + case 'A': + case 'V': + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - } - break; - case 'SubjectDistanceRange': - switch ( $val ) { - case 0: case 1: case 2: case 3: - $val = self::msg( $tag, $val ); + case 'GPSMeasureMode': + switch ( $val ) { + case 2: + case 3: + $val = $this->exifMsg( $tag, $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } break; - default: - /* If not recognized, display as is. */ + + case 'GPSTrackRef': + case 'GPSImgDirectionRef': + case 'GPSDestBearingRef': + switch ( $val ) { + case 'T': + case 'M': + $val = $this->exifMsg( 'GPSDirection', $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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 ); + + case 'GPSLatitude': + case 'GPSDestLatitude': + $val = $this->formatCoords( $val, 'latitude' ); break; - default: - /* If not recognized, display as is. */ + case 'GPSLongitude': + case 'GPSDestLongitude': + $val = $this->formatCoords( $val, 'longitude' ); break; - } - break; - case 'GPSLongitudeRef': - case 'GPSDestLongitudeRef': - switch ( $val ) { - case 'E': case 'W': - $val = self::msg( 'GPSLongitude', $val ); - break; - default: - /* If not recognized, display as is. */ + case 'GPSSpeedRef': + switch ( $val ) { + case 'K': + case 'M': + case 'N': + $val = $this->exifMsg( 'GPSSpeed', $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } 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 'GPSDestDistanceRef': + switch ( $val ) { + case 'K': + case 'M': + case 'N': + $val = $this->exifMsg( 'GPSDestDistance', $val ); + break; + default: + /* If not recognized, display as is. */ + break; + } + break; - case 'GPSStatus': - switch ( $val ) { - case 'A': case 'V': - $val = self::msg( $tag, $val ); + case 'GPSDOP': + // See http://en.wikipedia.org/wiki/Dilution_of_precision_(GPS) + if ( $val <= 2 ) { + $val = $this->exifMsg( $tag, 'excellent', $this->formatNum( $val ) ); + } elseif ( $val <= 5 ) { + $val = $this->exifMsg( $tag, 'good', $this->formatNum( $val ) ); + } elseif ( $val <= 10 ) { + $val = $this->exifMsg( $tag, 'moderate', $this->formatNum( $val ) ); + } elseif ( $val <= 20 ) { + $val = $this->exifMsg( $tag, 'fair', $this->formatNum( $val ) ); + } else { + $val = $this->exifMsg( $tag, 'poor', $this->formatNum( $val ) ); + } break; - default: - /* If not recognized, display as is. */ + + // 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 = $this->exifMsg( $tag, '', $val ); break; - } - break; - case 'GPSMeasureMode': - switch ( $val ) { - case 2: case 3: - $val = self::msg( $tag, $val ); + case 'Software': + if ( is_array( $val ) ) { + //if its a software, version array. + $val = $this->msg( 'exif-software-version-value', $val[0], $val[1] )->text(); + } else { + $val = $this->exifMsg( $tag, '', $val ); + } break; - default: - /* If not recognized, display as is. */ + + case 'ExposureTime': + // Show the pretty fraction as well as decimal version + $val = $this->msg( 'exif-exposuretime-format', + $this->formatFraction( $val ), $this->formatNum( $val ) )->text(); break; - } - break; - - case 'GPSTrackRef': - case 'GPSImgDirectionRef': - case 'GPSDestBearingRef': - switch ( $val ) { - case 'T': case 'M': - $val = self::msg( 'GPSDirection', $val ); + 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 = $this->exifMsg( $tag, 'overflow' ); + } else { + $val = $this->formatNum( $val ); + } break; - default: - /* If not recognized, display as is. */ + case 'FNumber': + $val = $this->msg( 'exif-fnumber-format', + $this->formatNum( $val ) )->text(); 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 ); + + case 'FocalLength': + case 'FocalLengthIn35mmFilm': + $val = $this->msg( 'exif-focallength-format', + $this->formatNum( $val ) )->text(); break; - default: - /* If not recognized, display as is. */ + + 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 = $this->msg( 'exif-maxaperturevalue-value', + $this->formatNum( $val ), + $this->formatNum( $fNumber, 2 ) + )->text(); + } + } break; - } - break; - case 'GPSDestDistanceRef': - switch ( $val ) { - case 'K': case 'M': case 'N': - $val = self::msg( 'GPSDestDistance', $val ); + 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 = $this->exifMsg( + 'iimcategory', + $val + ); + } break; - default: - /* If not recognized, display as is. */ + case 'SubjectNewsCode': + // Essentially like iimCategory. + // 8 (numeric) digit hierarchical + // classification. We decode the + // first 2 digits, which provide + // a broad category. + $val = $this->convertNewsCode( $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 = wfMessage( 'exif-software-version-value', $val[0], $val[1] )->text(); - } else { - $val = self::msg( $tag, '', $val ); - } - break; - - case 'ExposureTime': - // Show the pretty fraction as well as decimal version - $val = wfMessage( 'exif-exposuretime-format', - self::formatFraction( $val ), self::formatNum( $val ) )->text(); - 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 = wfMessage( 'exif-fnumber-format', - self::formatNum( $val ) )->text(); - break; - - case 'FocalLength': case 'FocalLengthIn35mmFilm': - $val = wfMessage( 'exif-focallength-format', - self::formatNum( $val ) )->text(); - 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 = wfMessage( 'exif-maxaperturevalue-value', - self::formatNum( $val ), - self::formatNum( $fNumber, 2 ) - )->text(); + 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'; } - } - 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 + + if ( $urgency !== '' ) { + $val = $this->exifMsg( 'urgency', + $urgency, $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'; - } + } + break; - 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 ) . ' ' . wfMessage( 'unit-pixel' )->text(); - 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 ); + // Things that have a unit of pixels. + case 'OriginalImageHeight': + case 'OriginalImageWidth': + case 'PixelXDimension': + case 'PixelYDimension': + case 'ImageWidth': + case 'ImageLength': + $val = $this->formatNum( $val ) . ' ' . $this->msg( 'unit-pixel' )->text(); break; - default: + + // 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; - } - break; - case 'Copyrighted': - switch ( $val ) { - case 'True': case 'False': - $val = self::msg( $tag, $val ); + + case 'ObjectCycle': + switch ( $val ) { + case 'a': + case 'p': + case 'b': + $val = $this->exifMsg( $tag, $val ); + break; + default: + $val = htmlspecialchars( $val ); + break; + } + break; + case 'Copyrighted': + switch ( $val ) { + case 'True': + case 'False': + $val = $this->exifMsg( $tag, $val ); + break; + } + break; + case 'Rating': + if ( $val == '-1' ) { + $val = $this->exifMsg( $tag, 'rejected' ); + } else { + $val = $this->formatNum( $val ); + } break; - } - break; - case 'Rating': - if ( $val == '-1' ) { - $val = self::msg( $tag, 'rejected' ); - } else { - $val = self::formatNum( $val ); - } - break; - case 'LanguageCode': - $lang = Language::fetchLanguageName( strtolower( $val ), $wgLang->getCode() ); - if ( $lang ) { - $val = htmlspecialchars( $lang ); - } else { - $val = htmlspecialchars( $val ); - } - break; + case 'LanguageCode': + $lang = Language::fetchLanguageName( strtolower( $val ), $this->getLanguage()->getCode() ); + if ( $lang ) { + $val = htmlspecialchars( $lang ); + } else { + $val = htmlspecialchars( $val ); + } + break; - default: - $val = self::formatNum( $val ); - break; + default: + $val = $this->formatNum( $val ); + break; } } // End formatting values, start flattening arrays. - $vals = self::flattenArray( $vals, $type ); - + $vals = $this->flattenArrayReal( $vals, $type ); } + return $tags; } + /** + * Flatten an array, using the content language for any messages. + * + * @param array $vals Array of values + * @param string $type 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 bool $noHtml If to avoid returning anything resembling HTML. + * (Ugly hack for backwards compatibility with old MediaWiki). + * @param bool|IContextSource $context + * @return string Single value (in wiki-syntax). + * @since 1.23 + */ + public static function flattenArrayContentLang( $vals, $type = 'ul', + $noHtml = false, $context = false + ) { + global $wgContLang; + $obj = new FormatMetadata; + if ( $context ) { + $obj->setContext( $context ); + } + $context = new DerivativeContext( $obj->getContext() ); + $context->setLanguage( $wgContLang ); + $obj->setContext( $context ); + + return $obj->flattenArrayReal( $vals, $type, $noHtml ); + } + + /** + * Flatten an array, using the user language for any messages. + * + * @param array $vals Array of values + * @param string $type 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 bool $noHtml If to avoid returning anything resembling HTML. + * (Ugly hack for backwards compatibility with old MediaWiki). + * @param bool|IContextSource $context + * @return string Single value (in wiki-syntax). + */ + public static function flattenArray( $vals, $type = 'ul', $noHtml = false, $context = false ) { + $obj = new FormatMetadata; + if ( $context ) { + $obj->setContext( $context ); + } + + return $obj->flattenArrayReal( $vals, $type, $noHtml ); + } + /** * 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 array $vals array of values + * @param array $vals Array of values * @param string $type 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). + * 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 bool $noHtml If to avoid returning anything resembling HTML. + * (Ugly hack for backwards compatibility with old mediawiki). + * @return string Single value (in wiki-syntax). + * @since 1.23 */ - public static function flattenArray( $vals, $type = 'ul', $noHtml = false ) { + public function flattenArrayReal( $vals, $type = 'ul', $noHtml = false ) { + if ( !is_array( $vals ) ) { + return $vals; // do nothing if not an array; + } + if ( isset( $vals['_type'] ) ) { $type = $vals['_type']; unset( $vals['_type'] ); @@ -849,105 +1053,118 @@ class FormatMetadata { if ( !is_array( $vals ) ) { return $vals; // do nothing if not an array; - } - elseif ( count( $vals ) === 1 && $type !== 'lang' ) { + } elseif ( count( $vals ) === 1 && $type !== 'lang' ) { return $vals[0]; - } - elseif ( count( $vals ) === 0 ) { + } elseif ( count( $vals ) === 0 ) { wfDebug( __METHOD__ . " metadata array with 0 elements!\n" ); + return ""; // paranoia. This should never happen - } - /* @todo 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 { - global $wgContLang; + } else { + /* @todo 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 + */ switch ( $type ) { - case 'lang': - // 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; + case 'lang': + // Display default, followed by ContLang, + // followed by the rest in no particular + // order. + + // Todo: hide some items if really long list. + + $content = ''; + + $priorityLanguages = $this->getPriorityLanguages(); + $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'] ); + } + foreach ( $priorityLanguages as $pLang ) { + if ( isset( $vals[$pLang] ) ) { + $isDefault = false; + if ( $vals[$pLang] === $defaultItem ) { + $defaultItem = false; + $isDefault = true; + } + $content .= $this->langItem( + $vals[$pLang], $pLang, + $isDefault, $noHtml ); + + unset( $vals[$pLang] ); + + if ( $this->singleLang ) { + return Html::rawElement( 'span', + array( 'lang' => $pLang ), $vals[$pLang] ); + } + } } - $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; + // Now do the rest. + foreach ( $vals as $lang => $item ) { + if ( $item === $defaultItem ) { + $defaultLang = $lang; + continue; + } + $content .= $this->langItem( $item, + $lang, false, $noHtml ); + if ( $this->singleLang ) { + return Html::rawElement( 'span', + array( 'lang' => $lang ), $item ); + } } - $content .= self::langItem( $item, - $lang, false, $noHtml ); - } - if ( $defaultItem !== false ) { - $content = self::langItem( $defaultItem, - $defaultLang, true, $noHtml ) . - $content; - } - if ( $noHtml ) { - return $content; - } - return '