diff options
Diffstat (limited to 'includes/media')
27 files changed, 4217 insertions, 2556 deletions
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 ) === '<?xml' ) { + // Old style. Not serialized but instead just a raw string of XML. + return $metadata; + } + + wfSuppressWarnings(); + $unser = unserialize( $metadata ); + wfRestoreWarnings(); + if ( is_array( $unser ) ) { + if ( isset( $unser['error'] ) ) { + return false; + } elseif ( isset( $unser['xml'] ) ) { + return $unser['xml']; + } else { + // Should never ever reach here. + throw new MWException( "Error unserializing DjVu metadata." ); + } + } + + // unserialize failed. Guess it wasn't really serialized after all, + return $metadata; + } + + /** * Cache a document tree for the DjVu XML metadata - * @param $image File - * @param $gettext Boolean: DOCUMENT (Default: false) - * @return bool + * @param File $image + * @param bool $gettext DOCUMENT (Default: false) + * @return bool|SimpleXMLElement */ function getMetaTree( $image, $gettext = false ) { - if ( isset( $image->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 <brion@pobox.com> - * 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 @@ -35,6 +35,11 @@ */ class DjVuImage { /** + * @const DJVUTXT_MEMORY_LIMIT Memory limit for the DjVu description software + */ + const DJVUTXT_MEMORY_LIMIT = 300000; + + /** * Constructor * * @param string $filename The DjVu file name. @@ -44,22 +49,18 @@ class DjVuImage { } /** - * @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 '<PAGE value="' . htmlspecialchars( UtfNormal::cleanUp( $matches[1] ) ) . '" />'; + $val = htmlspecialchars( UtfNormal::cleanUp( stripcslashes( $matches[1] ) ) ); + $val = str_replace( array( "\n", '�' ), array( ' ', '' ), $val ); + return '<PAGE value="' . $val . '" />'; } /** * 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 .= "</BODY>\n</DjVuXML>\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 <num>/<denom> 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 <num>/<denom> 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 '<ul class="metadata-langlist">' . + if ( $defaultItem !== false ) { + $content = $this->langItem( $defaultItem, + $defaultLang, true, $noHtml ) . + $content; + if ( $this->singleLang ) { + return $defaultItem; + } + } + if ( $noHtml ) { + return $content; + } + + return '<ul class="metadata-langlist">' . $content . '</ul>'; - case 'ol': - if ( $noHtml ) { - return "\n#" . implode( "\n#", $vals ); - } - return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>'; - case 'ul': - default: - if ( $noHtml ) { - return "\n*" . implode( "\n*", $vals ); - } - return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>'; + case 'ol': + if ( $noHtml ) { + return "\n#" . implode( "\n#", $vals ); + } + + return "<ol><li>" . implode( "</li>\n<li>", $vals ) . '</li></ol>'; + case 'ul': + default: + if ( $noHtml ) { + return "\n*" . implode( "\n*", $vals ); + } + + return "<ul><li>" . implode( "</li>\n<li>", $vals ) . '</li></ul>'; } } } /** Helper function for creating lists of translations. * - * @param string $value value (this is not escaped) - * @param string $lang lang code of item or false - * @param $default Boolean if it is default value. - * @param $noHtml Boolean If to avoid html (for back-compat) + * @param string $value Value (this is not escaped) + * @param string $lang Lang code of item or false + * @param bool $default If it is default value. + * @param bool $noHtml If to avoid html (for back-compat) * @throws MWException - * @return string language item (Note: despite how this looks, - * this is treated as wikitext not html). + * @return string Language item (Note: despite how this looks, this is + * treated as wikitext, not as HTML). */ - private static function langItem( $value, $lang, $default = false, $noHtml = false ) { + private function langItem( $value, $lang, $default = false, $noHtml = false ) { if ( $lang === false && $default === false ) { throw new MWException( '$lang and $default cannot both ' . 'be false.' ); @@ -961,13 +1178,13 @@ class FormatMetadata { } if ( $lang === false ) { + $msg = $this->msg( 'metadata-langitem-default', $wrappedValue ); if ( $noHtml ) { - return wfMessage( 'metadata-langitem-default', - $wrappedValue )->text() . "\n\n"; + return $msg->text() . "\n\n"; } /* else */ + return '<li class="mw-metadata-lang-default">' - . wfMessage( 'metadata-langitem-default', - $wrappedValue )->text() + . $msg->text() . "</li>\n"; } @@ -984,9 +1201,9 @@ class FormatMetadata { } // else we have a language specified + $msg = $this->msg( 'metadata-langitem', $wrappedValue, $langName, $lang ); if ( $noHtml ) { - return '*' . wfMessage( 'metadata-langitem', - $wrappedValue, $langName, $lang )->text(); + return '*' . $msg->text(); } /* else: */ $item = '<li class="mw-metadata-lang-code-' @@ -995,49 +1212,48 @@ class FormatMetadata { $item .= ' mw-metadata-lang-default'; } $item .= '" lang="' . $lang . '">'; - $item .= wfMessage( 'metadata-langitem', - $wrappedValue, $langName, $lang )->text(); + $item .= $msg->text(); $item .= "</li>\n"; + return $item; } /** * Convenience function for getFormattedData() * - * @private - * - * @param string $tag the tag name to pass on - * @param string $val the value of the tag - * @param string $arg an argument to pass ($1) - * @param string $arg2 a 2nd argument to pass ($2) - * @return string A wfMessage of "exif-$tag-$val" in lower case + * @param string $tag The tag name to pass on + * @param string $val The value of the tag + * @param string $arg An argument to pass ($1) + * @param string $arg2 A 2nd argument to pass ($2) + * @return string The text content of "exif-$tag-$val" message in lower case */ - static function msg( $tag, $val, $arg = null, $arg2 = null ) { + private function exifMsg( $tag, $val, $arg = null, $arg2 = null ) { global $wgContLang; if ( $val === '' ) { $val = 'value'; } - return wfMessage( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text(); + + return $this->msg( $wgContLang->lc( "exif-$tag-$val" ), $arg, $arg2 )->text(); } /** * Format a number, convert numbers from fractions into floating point * numbers, joins arrays of numbers with commas. * - * @param $num Mixed: the value to format - * @param $round float|int|bool digits to round to or false. + * @param mixed $num The value to format + * @param float|int|bool $round Digits to round to or false. * @return mixed A floating point number or whatever we were fed */ - static function formatNum( $num, $round = false ) { - global $wgLang; + private function formatNum( $num, $round = false ) { $m = array(); if ( is_array( $num ) ) { $out = array(); foreach ( $num as $number ) { - $out[] = self::formatNum( $number ); + $out[] = $this->formatNum( $number ); } - return $wgLang->commaList( $out ); + + return $this->getLanguage()->commaList( $out ); } if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { if ( $m[2] != 0 ) { @@ -1049,46 +1265,45 @@ class FormatMetadata { $newNum = $num; } - return $wgLang->formatNum( $newNum ); + return $this->getLanguage()->formatNum( $newNum ); } else { if ( is_numeric( $num ) && $round !== false ) { $num = round( $num, $round ); } - return $wgLang->formatNum( $num ); + + return $this->getLanguage()->formatNum( $num ); } } /** * Format a rational number, reducing fractions * - * @private - * - * @param $num Mixed: the value to format + * @param mixed $num The value to format * @return mixed A floating point number or whatever we were fed */ - static function formatFraction( $num ) { + private function formatFraction( $num ) { $m = array(); if ( preg_match( '/^(-?\d+)\/(\d+)$/', $num, $m ) ) { $numerator = intval( $m[1] ); $denominator = intval( $m[2] ); - $gcd = self::gcd( abs( $numerator ), $denominator ); + $gcd = $this->gcd( abs( $numerator ), $denominator ); if ( $gcd != 0 ) { // 0 shouldn't happen! ;) - return self::formatNum( $numerator / $gcd ) . '/' . self::formatNum( $denominator / $gcd ); + return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd ); } } - return self::formatNum( $num ); + + return $this->formatNum( $num ); } /** * Calculate the greatest common divisor of two integers. * - * @param $a Integer: Numerator - * @param $b Integer: Denominator + * @param int $a Numerator + * @param int $b Denominator * @return int - * @private */ - static function gcd( $a, $b ) { + private function gcd( $a, $b ) { /* // http://en.wikipedia.org/wiki/Euclidean_algorithm // Recursive form would be: @@ -1104,6 +1319,7 @@ class FormatMetadata { $a = $b; $b = $remainder; } + return $a; } @@ -1119,7 +1335,7 @@ class FormatMetadata { * @param string $val The 8 digit news code. * @return string The human readable form */ - private static function convertNewsCode( $val ) { + private function convertNewsCode( $val ) { if ( !preg_match( '/^\d{8}$/D', $val ) ) { // Not a valid news code. return $val; @@ -1179,9 +1395,10 @@ class FormatMetadata { break; } if ( $cat !== '' ) { - $catMsg = self::msg( 'iimcategory', $cat ); - $val = self::msg( 'subjectnewscode', '', $val, $catMsg ); + $catMsg = $this->exifMsg( 'iimcategory', $cat ); + $val = $this->exifMsg( 'subjectnewscode', '', $val, $catMsg ); } + return $val; } @@ -1189,11 +1406,11 @@ class FormatMetadata { * Format a coordinate value, convert numbers from floating point * into degree minute second representation. * - * @param int $coord degrees, minutes and seconds - * @param string $type latitude or longitude (for if its a NWS or E) + * @param int $coord Degrees, minutes and seconds + * @param string $type Latitude or longitude (for if its a NWS or E) * @return mixed A floating point number or whatever we were fed */ - static function formatCoords( $coord, $type ) { + private function formatCoords( $coord, $type ) { $ref = ''; if ( $coord < 0 ) { $nCoord = -$coord; @@ -1215,28 +1432,28 @@ class FormatMetadata { $min = floor( ( $nCoord - $deg ) * 60.0 ); $sec = round( ( ( $nCoord - $deg ) - $min / 60 ) * 3600, 2 ); - $deg = self::formatNum( $deg ); - $min = self::formatNum( $min ); - $sec = self::formatNum( $sec ); + $deg = $this->formatNum( $deg ); + $min = $this->formatNum( $min ); + $sec = $this->formatNum( $sec ); - return wfMessage( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text(); + return $this->msg( 'exif-coordinate-format', $deg, $min, $sec, $ref, $coord )->text(); } /** * Format the contact info field into a single value. * - * @param array $vals array with fields of the ContactInfo - * struct defined in the IPTC4XMP spec. Or potentially - * an array with one element that is a free form text - * value from the older iptc iim 1:118 prop. - * * This function might be called from * JpegHandler::convertMetadataVersion which is why it is * public. * - * @return String of html-ish looking wikitext + * @param array $vals Array with fields of the ContactInfo + * struct defined in the IPTC4XMP spec. Or potentially + * an array with one element that is a free form text + * value from the older iptc iim 1:118 prop. + * @return string HTML-ish looking wikitext + * @since 1.23 no longer static */ - public static function collapseContactInfo( $vals ) { + public function collapseContactInfo( $vals ) { if ( !( isset( $vals['CiAdrExtadr'] ) || isset( $vals['CiAdrCity'] ) || isset( $vals['CiAdrCtry'] ) @@ -1258,7 +1475,8 @@ class FormatMetadata { foreach ( $vals as &$val ) { $val = htmlspecialchars( $val ); } - return self::flattenArray( $vals ); + + return $this->flattenArrayReal( $vals ); } else { // We have a real ContactInfo field. // Its unclear if all these fields have to be @@ -1308,10 +1526,10 @@ class FormatMetadata { $emails[] = $finalEmail; } else { $emails[] = '[mailto:' - . $finalEmail - . ' <span class="email">' - . $finalEmail - . '</span>]'; + . $finalEmail + . ' <span class="email">' + . $finalEmail + . '</span>]'; } } } @@ -1340,34 +1558,315 @@ class FormatMetadata { . htmlspecialchars( $vals['CiUrlWork'] ) . '</span>'; } - return wfMessage( 'exif-contact-value', $email, $url, + + return $this->msg( 'exif-contact-value', $email, $url, $street, $city, $region, $postal, $country, $tel )->text(); } } -} -/** For compatability with old FormatExif class - * which some extensions use. - * - * @deprecated since 1.18 - * - */ -class FormatExif { - var $meta; + /** + * Get a list of fields that are visible by default. + * + * @return array + * @since 1.23 + */ + public static function getVisibleFields() { + $fields = array(); + $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() ); + foreach ( $lines as $line ) { + $matches = array(); + if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { + $fields[] = $matches[1]; + } + } + $fields = array_map( 'strtolower', $fields ); + + return $fields; + } + + /** + * Get an array of extended metadata. (See the imageinfo API for format.) + * + * @param File $file File to use + * @return array [<property name> => ['value' => <value>]], or [] on error + * @since 1.23 + */ + public function fetchExtendedMetadata( File $file ) { + global $wgMemc; + + wfProfileIn( __METHOD__ ); + + // If revision deleted, exit immediately + if ( $file->isDeleted( File::DELETED_FILE ) ) { + wfProfileOut( __METHOD__ ); + + return array(); + } + + $cacheKey = wfMemcKey( + 'getExtendedMetadata', + $this->getLanguage()->getCode(), + (int)$this->singleLang, + $file->getSha1() + ); + + $cachedValue = $wgMemc->get( $cacheKey ); + if ( + $cachedValue + && wfRunHooks( 'ValidateExtendedMetadataCache', array( $cachedValue['timestamp'], $file ) ) + ) { + $extendedMetadata = $cachedValue['data']; + } else { + $maxCacheTime = ( $file instanceof ForeignAPIFile ) ? 60 * 60 * 12 : 60 * 60 * 24 * 30; + $fileMetadata = $this->getExtendedMetadataFromFile( $file ); + $extendedMetadata = $this->getExtendedMetadataFromHook( $file, $fileMetadata, $maxCacheTime ); + if ( $this->singleLang ) { + $this->resolveMultilangMetadata( $extendedMetadata ); + } + // Make sure the metadata won't break the API when an XML format is used. + // This is an API-specific function so it would be cleaner to call it from + // outside fetchExtendedMetadata, but this way we don't need to redo the + // computation on a cache hit. + $this->sanitizeArrayForXml( $extendedMetadata ); + $valueToCache = array( 'data' => $extendedMetadata, 'timestamp' => wfTimestampNow() ); + $wgMemc->set( $cacheKey, $valueToCache, $maxCacheTime ); + } + + wfProfileOut( __METHOD__ ); + + return $extendedMetadata; + } + + /** + * Get file-based metadata in standardized format. + * + * Note that for a remote file, this might return metadata supplied by extensions. + * + * @param File $file File to use + * @return array [<property name> => ['value' => <value>]], or [] on error + * @since 1.23 + */ + protected function getExtendedMetadataFromFile( File $file ) { + // If this is a remote file accessed via an API request, we already + // have remote metadata so we just ignore any local one + if ( $file instanceof ForeignAPIFile ) { + // In case of error we pretend no metadata - this will get cached. + // Might or might not be a good idea. + return $file->getExtendedMetadata() ?: array(); + } + + wfProfileIn( __METHOD__ ); + + $uploadDate = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); + + $fileMetadata = array( + // This is modification time, which is close to "upload" time. + 'DateTime' => array( + 'value' => $uploadDate, + 'source' => 'mediawiki-metadata', + ), + ); + + $title = $file->getTitle(); + if ( $title ) { + $text = $title->getText(); + $pos = strrpos( $text, '.' ); + + if ( $pos ) { + $name = substr( $text, 0, $pos ); + } else { + $name = $text; + } + + $fileMetadata['ObjectName'] = array( + 'value' => $name, + 'source' => 'mediawiki-metadata', + ); + } + + $common = $file->getCommonMetaArray(); + + if ( $common !== false ) { + foreach ( $common as $key => $value ) { + $fileMetadata[$key] = array( + 'value' => $value, + 'source' => 'file-metadata', + ); + } + } + + wfProfileOut( __METHOD__ ); + + return $fileMetadata; + } + + /** + * Get additional metadata from hooks in standardized format. + * + * @param File $file File to use + * @param array $extendedMetadata + * @param int $maxCacheTime Hook handlers might use this parameter to override cache time + * + * @return array [<property name> => ['value' => <value>]], or [] on error + * @since 1.23 + */ + protected function getExtendedMetadataFromHook( File $file, array $extendedMetadata, + &$maxCacheTime + ) { + wfProfileIn( __METHOD__ ); + + wfRunHooks( 'GetExtendedMetadata', array( + &$extendedMetadata, + $file, + $this->getContext(), + $this->singleLang, + &$maxCacheTime + ) ); + + $visible = array_flip( self::getVisibleFields() ); + foreach ( $extendedMetadata as $key => $value ) { + if ( !isset( $visible[strtolower( $key )] ) ) { + $extendedMetadata[$key]['hidden'] = ''; + } + } + + wfProfileOut( __METHOD__ ); + + return $extendedMetadata; + } /** - * @param $meta array + * Turns an XMP-style multilang array into a single value. + * If the value is not a multilang array, it is returned unchanged. + * See mediawiki.org/wiki/Manual:File_metadata_handling#Multi-language_array_format + * @param mixed $value + * @return mixed Value in best language, null if there were no languages at all + * @since 1.23 */ - function FormatExif( $meta ) { - wfDeprecated( __METHOD__, '1.18' ); - $this->meta = $meta; + protected function resolveMultilangValue( $value ) { + if ( + !is_array( $value ) + || !isset( $value['_type'] ) + || $value['_type'] != 'lang' + ) { + return $value; // do nothing if not a multilang array + } + + // choose the language best matching user or site settings + $priorityLanguages = $this->getPriorityLanguages(); + foreach ( $priorityLanguages as $lang ) { + if ( isset( $value[$lang] ) ) { + return $value[$lang]; + } + } + + // otherwise go with the default language, if set + if ( isset( $value['x-default'] ) ) { + return $value['x-default']; + } + + // otherwise just return any one language + unset( $value['_type'] ); + if ( !empty( $value ) ) { + return reset( $value ); + } + + // this should not happen; signal error + return null; + } + + /** + * Takes an array returned by the getExtendedMetadata* functions, + * and resolves multi-language values in it. + * @param array $metadata + * @since 1.23 + */ + protected function resolveMultilangMetadata( &$metadata ) { + if ( !is_array( $metadata ) ) { + return; + } + foreach ( $metadata as &$field ) { + if ( isset( $field['value'] ) ) { + $field['value'] = $this->resolveMultilangValue( $field['value'] ); + } + } + } + + /** + * Makes sure the given array is a valid API response fragment + * (can be transformed into XML) + * @param array $arr + */ + protected function sanitizeArrayForXml( &$arr ) { + if ( !is_array( $arr ) ) { + return; + } + + $counter = 1; + foreach ( $arr as $key => &$value ) { + $sanitizedKey = $this->sanitizeKeyForXml( $key ); + if ( $sanitizedKey !== $key ) { + if ( isset( $arr[$sanitizedKey] ) ) { + // Make the sanitized keys hopefully unique. + // To make it definitely unique would be too much effort, given that + // sanitizing is only needed for misformatted metadata anyway, but + // this at least covers the case when $arr is numeric. + $sanitizedKey .= $counter; + ++$counter; + } + $arr[$sanitizedKey] = $arr[$key]; + unset( $arr[$key] ); + } + if ( is_array( $value ) ) { + $this->sanitizeArrayForXml( $value ); + } + } + } + + /** + * Turns a string into a valid XML identifier. + * Used to ensure that keys of an associative array in the + * API response do not break the XML formatter. + * @param string $key + * @return string + * @since 1.23 + */ + protected function sanitizeKeyForXml( $key ) { + // drop all characters which are not valid in an XML tag name + // a bunch of non-ASCII letters would be valid but probably won't + // be used so we take the easy way + $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key ); + // drop characters which are invalid at the first position + $key = preg_replace( '/^[\d-.]+/', '', $key ); + + if ( $key == '' ) { + $key = '_'; + } + + // special case for an internal keyword + if ( $key == '_element' ) { + $key = 'element'; + } + + return $key; } /** + * Returns a list of languages (first is best) to use when formatting multilang fields, + * based on user and site preferences. * @return array + * @since 1.23 */ - function getFormattedData() { - return FormatMetadata::getFormattedData( $this->meta ); + protected function getPriorityLanguages() { + $priorityLanguages = + Language::getFallbacksIncludingSiteLanguage( $this->getLanguage()->getCode() ); + $priorityLanguages = array_merge( + (array)$this->getLanguage()->getCode(), + $priorityLanguages[0], + $priorityLanguages[1] + ); + + return $priorityLanguages; } } diff --git a/includes/media/GIF.php b/includes/media/GIF.php index 608fb257..5992be11 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -27,7 +27,6 @@ * @ingroup Media */ class GIFHandler extends BitmapHandler { - const BROKEN_FILE = '0'; // value to store in img_metadata if error extracting metadata. function getMetadata( $image, $filename ) { @@ -36,6 +35,7 @@ class GIFHandler extends BitmapHandler { } catch ( Exception $e ) { // Broken file? wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); + return self::BROKEN_FILE; } @@ -43,35 +43,49 @@ class GIFHandler extends BitmapHandler { } /** - * @param $image File + * @param File $image * @return array|bool */ function formatMetadata( $image ) { + $meta = $this->getCommonMetaArray( $image ); + if ( count( $meta ) === 0 ) { + return false; + } + + return $this->formatMetadataHelper( $meta ); + } + + /** + * Return the standard metadata elements for #filemetadata parser func. + * @param File $image + * @return array|bool + */ + public function getCommonMetaArray( File $image ) { $meta = $image->getMetadata(); if ( !$meta ) { - return false; + return array(); } $meta = unserialize( $meta ); - if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) { - return false; + if ( !isset( $meta['metadata'] ) ) { + return array(); } + unset( $meta['metadata']['_MW_GIF_VERSION'] ); - if ( isset( $meta['metadata']['_MW_GIF_VERSION'] ) ) { - unset( $meta['metadata']['_MW_GIF_VERSION'] ); - } - return $this->formatMetadataHelper( $meta['metadata'] ); + return $meta['metadata']; } /** - * @param $image File - * @todo unittests + * @todo Add unit tests + * + * @param File $image * @return bool */ function getImageArea( $image ) { $ser = $image->getMetadata(); if ( $ser ) { $metadata = unserialize( $ser ); + return $image->getWidth() * $image->getHeight() * $metadata['frameCount']; } else { return $image->getWidth() * $image->getHeight(); @@ -79,7 +93,7 @@ class GIFHandler extends BitmapHandler { } /** - * @param $image File + * @param File $image * @return bool */ function isAnimatedImage( $image ) { @@ -90,6 +104,7 @@ class GIFHandler extends BitmapHandler { return true; } } + return false; } @@ -101,6 +116,7 @@ class GIFHandler extends BitmapHandler { function canAnimateThumbnail( $file ) { global $wgMaxAnimatedGifArea; $answer = $this->getImageArea( $file ) <= $wgMaxAnimatedGifArea; + return $answer; } @@ -120,19 +136,23 @@ class GIFHandler extends BitmapHandler { if ( !$data || !is_array( $data ) ) { wfDebug( __METHOD__ . " invalid GIF metadata\n" ); + return self::METADATA_BAD; } if ( !isset( $data['metadata']['_MW_GIF_VERSION'] ) - || $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION ) { + || $data['metadata']['_MW_GIF_VERSION'] != GIFMetadataExtractor::VERSION + ) { wfDebug( __METHOD__ . " old but compatible GIF metadata\n" ); + return self::METADATA_COMPATIBLE; } + return self::METADATA_GOOD; } /** - * @param $image File + * @param File $image * @return string */ function getLongDesc( $image ) { diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php index 887afa3f..178b0bf7 100644 --- a/includes/media/GIFMetadataExtractor.php +++ b/includes/media/GIFMetadataExtractor.php @@ -32,9 +32,14 @@ * @ingroup Media */ class GIFMetadataExtractor { - static $gif_frame_sep; - static $gif_extension_sep; - static $gif_term; + /** @var string */ + private static $gifFrameSep; + + /** @var string */ + private static $gifExtensionSep; + + /** @var string */ + private static $gifTerm; const VERSION = 1; @@ -45,13 +50,13 @@ class GIFMetadataExtractor { /** * @throws Exception - * @param $filename string + * @param string $filename * @return array */ static function getMetadata( $filename ) { - self::$gif_frame_sep = pack( "C", ord( "," ) ); - self::$gif_extension_sep = pack( "C", ord( "!" ) ); - self::$gif_term = pack( "C", ord( ";" ) ); + self::$gifFrameSep = pack( "C", ord( "," ) ); + self::$gifExtensionSep = pack( "C", ord( "!" ) ); + self::$gifTerm = pack( "C", ord( ";" ) ); $frameCount = 0; $duration = 0.0; @@ -93,7 +98,7 @@ class GIFMetadataExtractor { while ( !feof( $fh ) ) { $buf = fread( $fh, 1 ); - if ( $buf == self::$gif_frame_sep ) { + if ( $buf == self::$gifFrameSep ) { // Found a frame $frameCount++; @@ -108,7 +113,7 @@ class GIFMetadataExtractor { self::readGCT( $fh, $bpp ); fread( $fh, 1 ); self::skipBlock( $fh ); - } elseif ( $buf == self::$gif_extension_sep ) { + } elseif ( $buf == self::$gifExtensionSep ) { $buf = fread( $fh, 1 ); if ( strlen( $buf ) < 1 ) { throw new Exception( "Ran out of input" ); @@ -163,8 +168,8 @@ class GIFMetadataExtractor { $commentCount = count( $comment ); if ( $commentCount === 0 - || $comment[$commentCount - 1] !== $data ) - { + || $comment[$commentCount - 1] !== $data + ) { // Some applications repeat the same comment on each // frame of an animated GIF image, so if this comment // is identical to the last, only extract once. @@ -217,15 +222,14 @@ class GIFMetadataExtractor { $xmp = self::readBlock( $fh, true ); if ( substr( $xmp, -257, 3 ) !== "\x01\xFF\xFE" - || substr( $xmp, -4 ) !== "\x03\x02\x01\x00" ) - { + || substr( $xmp, -4 ) !== "\x03\x02\x01\x00" + ) { // this is just a sanity check. throw new Exception( "XMP does not have magic trailer!" ); } // strip out trailer. $xmp = substr( $xmp, 0, -257 ); - } else { // unrecognized extension block fseek( $fh, -( $blockLength + 1 ), SEEK_CUR ); @@ -235,7 +239,7 @@ class GIFMetadataExtractor { } else { self::skipBlock( $fh ); } - } elseif ( $buf == self::$gif_term ) { + } elseif ( $buf == self::$gifTerm ) { break; } else { if ( strlen( $buf ) < 1 ) { @@ -257,20 +261,21 @@ class GIFMetadataExtractor { } /** - * @param $fh - * @param $bpp + * @param resource $fh + * @param int $bpp * @return void */ static function readGCT( $fh, $bpp ) { if ( $bpp > 0 ) { - for ( $i = 1; $i <= pow( 2, $bpp ); ++$i ) { + $max = pow( 2, $bpp ); + for ( $i = 1; $i <= $max; ++$i ) { fread( $fh, 3 ); } } } /** - * @param $data + * @param string $data * @throws Exception * @return int */ @@ -289,7 +294,7 @@ class GIFMetadataExtractor { } /** - * @param $fh + * @param resource $fh * @throws Exception */ static function skipBlock( $fh ) { @@ -313,8 +318,8 @@ class GIFMetadataExtractor { * saying how long the sub-block is, followed by the sub-block. * The entire block is terminated by a sub-block of length * 0. - * @param $fh FileHandle - * @param $includeLengths Boolean Include the length bytes of the + * @param resource $fh File handle + * @param bool $includeLengths Include the length bytes of the * sub-blocks in the returned value. Normally this is false, * except XMP is weird and does a hack where you need to keep * these length bytes. @@ -341,7 +346,7 @@ class GIFMetadataExtractor { $data .= fread( $fh, ord( $subLength ) ); $subLength = fread( $fh, 1 ); } + return $data; } - } diff --git a/includes/media/IPTC.php b/includes/media/IPTC.php index 544dd211..478249fe 100644 --- a/includes/media/IPTC.php +++ b/includes/media/IPTC.php @@ -27,7 +27,6 @@ * @ingroup Media */ class IPTC { - /** * This takes the results of iptcparse() and puts it into a * form that can be handled by mediawiki. Generally called from @@ -35,14 +34,14 @@ class IPTC { * * @see http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf * - * @param string $rawData app13 block from jpeg containing iptc/iim data - * @return Array iptc metadata array + * @param string $rawData The app13 block from jpeg containing iptc/iim data + * @return array IPTC metadata array */ static function parse( $rawData ) { $parsed = iptcparse( $rawData ); - $data = Array(); + $data = array(); if ( !is_array( $parsed ) ) { - return $data; + return $data; } $c = ''; @@ -85,7 +84,8 @@ class IPTC { $titles = array(); } - for ( $i = 0; $i < count( $titles ); $i++ ) { + $titleCount = count( $titles ); + for ( $i = 0; $i < $titleCount; $i++ ) { if ( isset( $bylines[$i] ) ) { // theoretically this should always be set // but doesn't hurt to be careful. @@ -225,7 +225,7 @@ class IPTC { if ( isset( $parsed['2#060'] ) ) { $time = $parsed['2#060']; } else { - $time = Array(); + $time = array(); } $timestamp = self::timeHelper( $val, $time, $c ); if ( $timestamp ) { @@ -239,7 +239,7 @@ class IPTC { if ( isset( $parsed['2#063'] ) ) { $time = $parsed['2#063']; } else { - $time = Array(); + $time = array(); } $timestamp = self::timeHelper( $val, $time, $c ); if ( $timestamp ) { @@ -252,7 +252,7 @@ class IPTC { if ( isset( $parsed['2#035'] ) ) { $time = $parsed['2#035']; } else { - $time = Array(); + $time = array(); } $timestamp = self::timeHelper( $val, $time, $c ); if ( $timestamp ) { @@ -265,7 +265,7 @@ class IPTC { if ( isset( $parsed['2#038'] ) ) { $time = $parsed['2#038']; } else { - $time = Array(); + $time = array(); } $timestamp = self::timeHelper( $val, $time, $c ); if ( $timestamp ) { @@ -300,7 +300,7 @@ class IPTC { wfDebugLog( 'iptc', 'IPTC: ' . '2:04 too short. ' . 'Ignoring.' ); - break; + break; } $extracted = substr( $con[0], 4 ); $data['IntellectualGenre'] = $extracted; @@ -315,9 +315,7 @@ class IPTC { foreach ( $codes as $ic ) { $fields = explode( ':', $ic, 3 ); - if ( count( $fields ) < 2 || - $fields[0] !== 'IPTC' ) - { + if ( count( $fields ) < 2 || $fields[0] !== 'IPTC' ) { wfDebugLog( 'IPTC', 'IPTC: ' . 'Invalid 2:12 - ' . $ic ); break; @@ -341,11 +339,11 @@ class IPTC { break; default: - wfDebugLog( 'iptc', "Unsupported iptc tag: $tag. Value: " . implode( ',', $val )); + wfDebugLog( 'iptc', "Unsupported iptc tag: $tag. Value: " . implode( ',', $val ) ); break; } - } + return $data; } @@ -355,8 +353,8 @@ class IPTC { * @todo Potentially this should also capture the timezone offset. * @param array $date The date tag * @param array $time The time tag - * @param $c - * @return String Date in exif format. + * @param string $c The charset + * @return string Date in EXIF format. */ private static function timeHelper( $date, $time, $c ) { if ( count( $date ) === 1 ) { @@ -387,12 +385,14 @@ class IPTC { // April, but the year and day is unknown. We don't process these // types of incomplete dates atm. wfDebugLog( 'iptc', "IPTC: invalid time ( $time ) or date ( $date )" ); + return null; } - $unixTS = wfTimestamp( TS_UNIX, $date . substr( $time, 0, 6 )); + $unixTS = wfTimestamp( TS_UNIX, $date . substr( $time, 0, 6 ) ); if ( $unixTS === false ) { wfDebugLog( 'iptc', "IPTC: can't convert date to TS_UNIX: $date $time." ); + return null; } @@ -400,12 +400,13 @@ class IPTC { + ( intval( substr( $time, 9, 2 ) ) * 60 ); if ( substr( $time, 6, 1 ) === '-' ) { - $tz = - $tz; + $tz = -$tz; } $finalTimestamp = wfTimestamp( TS_EXIF, $unixTS + $tz ); if ( $finalTimestamp === false ) { wfDebugLog( 'iptc', "IPTC: can't make final timestamp. Date: " . ( $unixTS + $tz ) ); + return null; } if ( $dateOnly ) { @@ -434,9 +435,10 @@ class IPTC { return $data; } + /** * Helper function of a helper function to convert charset for iptc values. - * @param $data Mixed String or Array: The iptc string + * @param string|array $data The IPTC string * @param string $charset The charset * * @return string @@ -461,13 +463,14 @@ class IPTC { return self::convIPTCHelper( $oldData, 'Windows-1252' ); } } + return trim( $data ); } /** * take the value of 1:90 tag and returns a charset * @param string $tag 1:90 tag. - * @return string charset name or "?" + * @return string Charset name or "?" * Warning, this function does not (and is not intended to) detect * all iso 2022 escape codes. In practise, the code for utf-8 is the * only code that seems to have wide use. It does detect that code. diff --git a/includes/media/ImageHandler.php b/includes/media/ImageHandler.php index 6794e4bf..6dd0453e 100644 --- a/includes/media/ImageHandler.php +++ b/includes/media/ImageHandler.php @@ -27,9 +27,8 @@ * @ingroup Media */ abstract class ImageHandler extends MediaHandler { - /** - * @param $file File + * @param File $file * @return bool */ function canRender( $file ) { @@ -60,6 +59,7 @@ abstract class ImageHandler extends MediaHandler { } else { throw new MWException( 'No width specified to ' . __METHOD__ ); } + # Removed for ProofreadPage #$width = intval( $width ); return "{$width}px"; @@ -79,8 +79,8 @@ abstract class ImageHandler extends MediaHandler { } /** - * @param $image File - * @param $params + * @param File $image + * @param array $params * @return bool */ function normaliseParams( $image, &$params ) { @@ -141,20 +141,22 @@ abstract class ImageHandler extends MediaHandler { } if ( !$this->validateThumbParams( $params['physicalWidth'], - $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) ) { + $params['physicalHeight'], $srcWidth, $srcHeight, $mimeType ) + ) { return false; } + return true; } /** * Validate thumbnail parameters and fill in the correct height * - * @param $width Integer: specified width (input/output) - * @param $height Integer: height (output only) - * @param $srcWidth Integer: width of the source image - * @param $srcHeight Integer: height of the source image - * @param $mimeType + * @param int $width Specified width (input/output) + * @param int $height Height (output only) + * @param int $srcWidth Width of the source image + * @param int $srcHeight Height of the source image + * @param string $mimeType Unused * @return bool False to indicate that an error should be returned to the user. */ function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) { @@ -163,10 +165,12 @@ abstract class ImageHandler extends MediaHandler { # Sanity check $width if ( $width <= 0 ) { wfDebug( __METHOD__ . ": Invalid destination width: $width\n" ); + return false; } if ( $srcWidth <= 0 ) { wfDebug( __METHOD__ . ": Invalid source width: $srcWidth\n" ); + return false; } @@ -175,14 +179,15 @@ abstract class ImageHandler extends MediaHandler { # Force height to be at least 1 pixel $height = 1; } + return true; } /** - * @param $image File - * @param $script - * @param $params - * @return bool|ThumbnailImage + * @param File $image + * @param string $script + * @param array $params + * @return bool|MediaTransformOutput */ function getScriptedTransform( $image, $script, $params ) { if ( !$this->normaliseParams( $image, $params ) ) { @@ -199,8 +204,10 @@ abstract class ImageHandler extends MediaHandler { wfSuppressWarnings(); $gis = getimagesize( $path ); wfRestoreWarnings(); + return $gis; } + /** * Function that returns the number of pixels to be thumbnailed. * Intended for animated GIFs to multiply by the number of frames. @@ -214,21 +221,21 @@ abstract class ImageHandler extends MediaHandler { return $image->getWidth() * $image->getHeight(); } - /** - * @param $file File + * @param File $file * @return string */ function getShortDesc( $file ) { global $wgLang; $nbytes = htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); - $widthheight = wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->escaped(); + $widthheight = wfMessage( 'widthheight' ) + ->numParams( $file->getWidth(), $file->getHeight() )->escaped(); return "$widthheight ($nbytes)"; } /** - * @param $file File + * @param File $file * @return string */ function getLongDesc( $file ) { @@ -238,25 +245,44 @@ abstract class ImageHandler extends MediaHandler { if ( $pages === false || $pages <= 1 ) { $msg = wfMessage( 'file-info-size' )->numParams( $file->getWidth(), $file->getHeight() )->params( $size, - $file->getMimeType() )->parse(); + $file->getMimeType() )->parse(); } else { $msg = wfMessage( 'file-info-size-pages' )->numParams( $file->getWidth(), $file->getHeight() )->params( $size, - $file->getMimeType() )->numParams( $pages )->parse(); + $file->getMimeType() )->numParams( $pages )->parse(); } + return $msg; } /** - * @param $file File + * @param File $file * @return string */ function getDimensionsString( $file ) { $pages = $file->pageCount(); if ( $pages > 1 ) { - return wfMessage( 'widthheightpage' )->numParams( $file->getWidth(), $file->getHeight(), $pages )->text(); + return wfMessage( 'widthheightpage' ) + ->numParams( $file->getWidth(), $file->getHeight(), $pages )->text(); } else { - return wfMessage( 'widthheight' )->numParams( $file->getWidth(), $file->getHeight() )->text(); + return wfMessage( 'widthheight' ) + ->numParams( $file->getWidth(), $file->getHeight() )->text(); } } + + public function sanitizeParamsForBucketing( $params ) { + $params = parent::sanitizeParamsForBucketing( $params ); + + // We unset the height parameters in order to let normaliseParams recalculate them + // Otherwise there might be a height discrepancy + if ( isset( $params['height'] ) ) { + unset( $params['height'] ); + } + + if ( isset( $params['physicalHeight'] ) ) { + unset( $params['physicalHeight'] ); + } + + return $params; + } } diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index fa763668..fbdbdfe3 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -32,6 +32,70 @@ */ class JpegHandler extends ExifBitmapHandler { + function normaliseParams( $image, &$params ) { + if ( !parent::normaliseParams( $image, $params ) ) { + return false; + } + if ( isset( $params['quality'] ) && !self::validateQuality( $params['quality'] ) ) { + return false; + } + return true; + } + + function validateParam( $name, $value ) { + if ( $name === 'quality' ) { + return self::validateQuality( $value ); + } else { + return parent::validateParam( $name, $value ); + } + } + + /** Validate and normalize quality value to be between 1 and 100 (inclusive). + * @param int $value Quality value, will be converted to integer or 0 if invalid + * @return bool True if the value is valid + */ + private static function validateQuality( $value ) { + return $value === 'low'; + } + + function makeParamString( $params ) { + // Prepend quality as "qValue-". This has to match parseParamString() below + $res = parent::makeParamString( $params ); + if ( $res && isset( $params['quality'] ) ) { + $res = "q{$params['quality']}-$res"; + } + return $res; + } + + function parseParamString( $str ) { + // $str contains "qlow-200px" or "200px" strings because thumb.php would strip the filename + // first - check if the string begins with "qlow-", and if so, treat it as quality. + // Pass the first portion, or the whole string if "qlow-" not found, to the parent + // The parsing must match the makeParamString() above + $res = false; + $m = false; + if ( preg_match( '/q([^-]+)-(.*)$/', $str, $m ) ) { + $v = $m[1]; + if ( self::validateQuality( $v ) ) { + $res = parent::parseParamString( $m[2] ); + if ( $res ) { + $res['quality'] = $v; + } + } + } else { + $res = parent::parseParamString( $str ); + } + return $res; + } + + function getScriptParams( $params ) { + $res = parent::getScriptParams( $params ); + if ( isset( $params['quality'] ) ) { + $res['quality'] = $params['quality']; + } + return $res; + } + function getMetadata( $image, $filename ) { try { $meta = BitmapMetadataHandler::Jpeg( $filename ); @@ -40,10 +104,11 @@ class JpegHandler extends ExifBitmapHandler { throw new MWException( 'Metadata array is not an array' ); } $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version(); + return serialize( $meta ); - } - catch ( MWException $e ) { - // BitmapMetadataHandler throws an exception in certain exceptional cases like if file does not exist. + } catch ( MWException $e ) { + // BitmapMetadataHandler throws an exception in certain exceptional + // cases like if file does not exist. wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); /* This used to use 0 (ExifBitmapHandler::OLD_BROKEN_FILE) for the cases @@ -55,14 +120,15 @@ class JpegHandler extends ExifBitmapHandler { * Thus switch to using -1 to denote only a broken file, and use an array with only * MEDIAWIKI_EXIF_VERSION to denote no props. */ + return ExifBitmapHandler::BROKEN_FILE; } } /** - * @param $file File + * @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 */ @@ -79,16 +145,32 @@ class JpegHandler extends ExifBitmapHandler { wfDebug( __METHOD__ . ": running jpgtran: $cmd\n" ); wfProfileIn( 'jpegtran' ); $retval = 0; - $err = wfShellExecWithStderr( $cmd, $retval, $env ); + $err = wfShellExecWithStderr( $cmd, $retval ); wfProfileOut( 'jpegtran' ); if ( $retval !== 0 ) { $this->logErrorForExternalProcess( $retval, $err, $cmd ); + return new MediaTransformError( 'thumbnail_error', 0, 0, $err ); } + return false; } else { return parent::rotate( $file, $params ); } } + public function supportsBucketing() { + return true; + } + + public function sanitizeParamsForBucketing( $params ) { + $params = parent::sanitizeParamsForBucketing( $params ); + + // Quality needs to be cleared for bucketing. Buckets need to be default quality + if ( isset( $params['quality'] ) ) { + unset( $params['quality'] ); + } + + return $params; + } } diff --git a/includes/media/JpegMetadataExtractor.php b/includes/media/JpegMetadataExtractor.php index c7030eba..8c5b46bb 100644 --- a/includes/media/JpegMetadataExtractor.php +++ b/includes/media/JpegMetadataExtractor.php @@ -30,8 +30,8 @@ * @ingroup Media */ class JpegMetadataExtractor { - const MAX_JPEG_SEGMENTS = 200; + // the max segment is a sanity check. // A jpeg file should never even remotely have // that many segments. Your average file has about 10. @@ -43,9 +43,9 @@ class JpegMetadataExtractor { * but gis doesn't support having multiple app1 segments * and those can't extract xmp on files containing both exif and xmp data * - * @param string $filename name of jpeg file - * @return Array of interesting segments. - * @throws MWException if given invalid file. + * @param string $filename Name of jpeg file + * @return array Array of interesting segments. + * @throws MWException If given invalid file. */ static function segmentSplitter( $filename ) { $showXMP = function_exists( 'xml_parser_create_ns' ); @@ -83,7 +83,8 @@ class JpegMetadataExtractor { throw new MWException( 'Too many jpeg segments. Aborting' ); } if ( $buffer !== "\xFF" ) { - throw new MWException( "Error reading jpeg file marker. Expected 0xFF but got " . bin2hex( $buffer ) ); + throw new MWException( "Error reading jpeg file marker. " . + "Expected 0xFF but got " . bin2hex( $buffer ) ); } $buffer = fread( $fh, 1 ); @@ -113,7 +114,6 @@ class JpegMetadataExtractor { } else { wfDebug( __METHOD__ . " Ignoring JPEG comment as is garbage.\n" ); } - } elseif ( $buffer === "\xE1" ) { // APP1 section (Exif, XMP, and XMP extended) // only extract if XMP is enabled. @@ -160,7 +160,6 @@ class JpegMetadataExtractor { } fseek( $fh, $size['int'] - 2, SEEK_CUR ); } - } // shouldn't get here. throw new MWException( "Reached end of jpeg file unexpectedly" ); @@ -168,9 +167,9 @@ class JpegMetadataExtractor { /** * Helper function for jpegSegmentSplitter - * @param &$fh FileHandle for jpeg file + * @param resource &$fh File handle for JPEG file * @throws MWException - * @return string data content of segment. + * @return string Data content of segment. */ private static function jpegExtractMarker( &$fh ) { $size = wfUnpack( "nint", fread( $fh, 2 ), 2 ); @@ -181,6 +180,7 @@ class JpegMetadataExtractor { if ( strlen( $segment ) !== $size['int'] - 2 ) { throw new MWException( "Segment shorter than expected" ); } + return $segment; } @@ -193,9 +193,10 @@ class JpegMetadataExtractor { * * This should generally be called by BitmapMetadataHandler::doApp13() * - * @param string $app13 photoshop psir app13 block from jpg. + * @param string $app13 Photoshop psir app13 block from jpg. * @throws MWException (It gets caught next level up though) - * @return String if the iptc hash is good or not. + * @return string If the iptc hash is good or not. One of 'iptc-no-hash', + * 'iptc-good-hash', 'iptc-bad-hash'. */ public static function doPSIR( $app13 ) { if ( !$app13 ) { @@ -275,7 +276,6 @@ class JpegMetadataExtractor { $lenData['len']++; } $offset += $lenData['len']; - } if ( !$realHash || !$recordedHash ) { diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php index 779e23c9..64ca0115 100644 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -32,34 +32,46 @@ abstract class MediaHandler { const METADATA_BAD = false; const METADATA_COMPATIBLE = 2; // for old but backwards compatible. /** - * Instance cache + * Max length of error logged by logErrorForExternalProcess() */ - static $handlers = array(); + const MAX_ERR_LOG_SIZE = 65535; + + /** @var MediaHandler[] Instance cache with array of MediaHandler */ + protected static $handlers = array(); /** * Get a MediaHandler for a given MIME type from the instance cache * - * @param $type string - * + * @param string $type * @return MediaHandler */ static function getHandler( $type ) { global $wgMediaHandlers; if ( !isset( $wgMediaHandlers[$type] ) ) { wfDebug( __METHOD__ . ": no handler found for $type.\n" ); + return false; } $class = $wgMediaHandlers[$type]; if ( !isset( self::$handlers[$class] ) ) { self::$handlers[$class] = new $class; if ( !self::$handlers[$class]->isEnabled() ) { + wfDebug( __METHOD__ . ": $class is not enabled\n" ); self::$handlers[$class] = false; } } + return self::$handlers[$class]; } /** + * Resets all static caches + */ + public static function resetCache() { + self::$handlers = array(); + } + + /** * Get an associative array mapping magic word IDs to parameter names. * Will be used by the parser to identify parameters. */ @@ -70,24 +82,24 @@ abstract class MediaHandler { * Return true to accept the parameter, and false to reject it. * If you return false, the parser will do something quiet and forgiving. * - * @param $name - * @param $value + * @param string $name + * @param mixed $value */ abstract function validateParam( $name, $value ); /** * Merge a parameter array into a string appropriate for inclusion in filenames * - * @param $params array Array of parameters that have been through normaliseParams. - * @return String + * @param array $params Array of parameters that have been through normaliseParams. + * @return string */ abstract function makeParamString( $params ); /** * Parse a param string made with makeParamString back into an array * - * @param $str string The parameter string without file name (e.g. 122px) - * @return Array|Boolean Array of parameters or false on failure. + * @param string $str The parameter string without file name (e.g. 122px) + * @return array|bool Array of parameters or false on failure. */ abstract function parseParamString( $str ); @@ -95,8 +107,8 @@ abstract class MediaHandler { * Changes the parameter array as necessary, ready for transformation. * Should be idempotent. * Returns false if the parameters are unacceptable and the transform should fail - * @param $image - * @param $params + * @param File $image + * @param array $params */ abstract function normaliseParams( $image, &$params ); @@ -104,19 +116,30 @@ abstract class MediaHandler { * Get an image size array like that returned by getimagesize(), or false if it * can't be determined. * - * @param $image File: the image object, or false if there isn't one - * @param string $path the filename - * @return Array Follow the format of PHP getimagesize() internal function. See http://www.php.net/getimagesize + * This function is used for determining the width, height and bitdepth directly + * from an image. The results are stored in the database in the img_width, + * img_height, img_bits fields. + * + * @note If this is a multipage file, return the width and height of the + * first page. + * + * @param File $image The image object, or false if there isn't one + * @param string $path The filename + * @return array Follow the format of PHP getimagesize() internal function. + * See http://www.php.net/getimagesize. MediaWiki will only ever use the + * first two array keys (the width and height), and the 'bits' associative + * key. All other array keys are ignored. Returning a 'bits' key is optional + * as not all formats have a notion of "bitdepth". */ abstract function getImageSize( $image, $path ); /** * Get handler-specific metadata which will be saved in the img_metadata field. * - * @param $image File: the image object, or false if there isn't one. + * @param File $image The image object, or false if there isn't one. * Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!) - * @param string $path the filename - * @return String + * @param string $path The filename + * @return string A string of metadata in php serialized form (Run through serialize()) */ function getMetadata( $image, $path ) { return ''; @@ -127,7 +150,7 @@ abstract class MediaHandler { * * This is not used for validating metadata, this is used for the api when returning * metadata, since api content formats should stay the same over time, and so things - * using ForiegnApiRepo can keep backwards compatibility + * using ForeignApiRepo can keep backwards compatibility * * All core media handlers share a common version number, and extensions can * use the GetMetadataVersion hook to append to the array (they should append a unique @@ -135,11 +158,12 @@ abstract class MediaHandler { * version 3 it might add to the end of the array the element 'foo=3'. if the core metadata * version is 2, the end version string would look like '2;foo=3'. * - * @return string version string + * @return string Version string */ static function getMetadataVersion() { - $version = Array( '2' ); // core metadata version - wfRunHooks( 'GetMetadataVersion', Array( &$version ) ); + $version = array( '2' ); // core metadata version + wfRunHooks( 'GetMetadataVersion', array( &$version ) ); + return implode( ';', $version ); } @@ -149,9 +173,9 @@ abstract class MediaHandler { * By default just returns $metadata, but can be used to allow * media handlers to convert between metadata versions. * - * @param $metadata Mixed String or Array metadata array (serialized if string) - * @param $version Integer target version - * @return Array serialized metadata in specified version, or $metadata on fail. + * @param string|array $metadata Metadata array (serialized if string) + * @param int $version Target version + * @return array Serialized metadata in specified version, or $metadata on fail. */ function convertMetadataVersion( $metadata, $version = 1 ) { if ( !is_array( $metadata ) ) { @@ -160,14 +184,18 @@ abstract class MediaHandler { wfSuppressWarnings(); $ret = unserialize( $metadata ); wfRestoreWarnings(); + return $ret; } + return $metadata; } /** * Get a string describing the type of metadata, for display purposes. * + * @note This method is currently unused. + * @param File $image * @return string */ function getMetadataType( $image ) { @@ -179,8 +207,15 @@ abstract class MediaHandler { * If it returns MediaHandler::METADATA_BAD (or false), Image * will reload the metadata from the file and update the database. * MediaHandler::METADATA_GOOD for if the metadata is a-ok, - * MediaHanlder::METADATA_COMPATIBLE if metadata is old but backwards + * MediaHandler::METADATA_COMPATIBLE if metadata is old but backwards * compatible (which may or may not trigger a metadata reload). + * + * @note Returning self::METADATA_BAD will trigger a metadata reload from + * file on page view. Always returning this from a broken file, or suddenly + * triggering as bad metadata for a large number of files can cause + * performance problems. + * @param File $image + * @param string $metadata The metadata in serialized form * @return bool */ function isMetadataValid( $image, $metadata ) { @@ -188,13 +223,52 @@ abstract class MediaHandler { } /** + * Get an array of standard (FormatMetadata type) metadata values. + * + * The returned data is largely the same as that from getMetadata(), + * but formatted in a standard, stable, handler-independent way. + * The idea being that some values like ImageDescription or Artist + * are universal and should be retrievable in a handler generic way. + * + * The specific properties are the type of properties that can be + * handled by the FormatMetadata class. These values are exposed to the + * user via the filemetadata parser function. + * + * Details of the response format of this function can be found at + * https://www.mediawiki.org/wiki/Manual:File_metadata_handling + * tl/dr: the response is an associative array of + * properties keyed by name, but the value can be complex. You probably + * want to call one of the FormatMetadata::flatten* functions on the + * property values before using them, or call + * FormatMetadata::getFormattedData() on the full response array, which + * transforms all values into prettified, human-readable text. + * + * Subclasses overriding this function must return a value which is a + * valid API response fragment (all associative array keys are valid + * XML tagnames). + * + * Note, if the file simply has no metadata, but the handler supports + * this interface, it should return an empty array, not false. + * + * @param File $file + * @return array|bool False if interface not supported + * @since 1.23 + */ + public function getCommonMetaArray( File $file ) { + return false; + } + + /** * Get a MediaTransformOutput object representing an alternate of the transformed * output which will call an intermediary thumbnail assist script. * * Used when the repository has a thumbnailScriptUrl option configured. * * Return false to fall back to the regular getTransform(). - * @return bool + * @param File $image + * @param string $script + * @param array $params + * @return bool|ThumbnailImage */ function getScriptedTransform( $image, $script, $params ) { return false; @@ -204,8 +278,8 @@ abstract class MediaHandler { * Get a MediaTransformOutput object representing the transformed output. Does not * actually do the transform. * - * @param $image File: the image object - * @param string $dstPath filesystem destination path + * @param File $image The image object + * @param string $dstPath Filesystem destination path * @param string $dstUrl Destination URL to use in output HTML * @param array $params Arbitrary set of parameters validated by $this->validateParam() * @return MediaTransformOutput @@ -218,13 +292,12 @@ abstract class MediaHandler { * Get a MediaTransformOutput object representing the transformed output. Does the * transform unless $flags contains self::TRANSFORM_LATER. * - * @param $image File: the image object - * @param string $dstPath filesystem destination path - * @param string $dstUrl destination URL to use in output HTML - * @param array $params arbitrary set of parameters validated by $this->validateParam() + * @param File $image The image object + * @param string $dstPath Filesystem destination path + * @param string $dstUrl Destination URL to use in output HTML + * @param array $params Arbitrary set of parameters validated by $this->validateParam() * Note: These parameters have *not* gone through $this->normaliseParams() - * @param $flags Integer: a bitfield, may contain self::TRANSFORM_LATER - * + * @param int $flags A bitfield, may contain self::TRANSFORM_LATER * @return MediaTransformOutput */ abstract function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ); @@ -232,31 +305,32 @@ abstract class MediaHandler { /** * Get the thumbnail extension and MIME type for a given source MIME type * - * @param String $ext Extension of original file - * @param String $mime Mime type of original file - * @param Array $params Handler specific rendering parameters - * @return array thumbnail extension and MIME type + * @param string $ext Extension of original file + * @param string $mime MIME type of original file + * @param array $params Handler specific rendering parameters + * @return array Thumbnail extension and MIME type */ function getThumbType( $ext, $mime, $params = null ) { $magic = MimeMagic::singleton(); if ( !$ext || $magic->isMatchingExtension( $ext, $mime ) === false ) { - // The extension is not valid for this mime type and we do - // recognize the mime type + // The extension is not valid for this MIME type and we do + // recognize the MIME type $extensions = $magic->getExtensionsForType( $mime ); if ( $extensions ) { return array( strtok( $extensions, ' ' ), $mime ); } } - // The extension is correct (true) or the mime type is unknown to + // The extension is correct (true) or the MIME type is unknown to // MediaWiki (null) return array( $ext, $mime ); } /** * Get useful response headers for GET/HEAD requests for a file with the given metadata - * @param $metadata mixed Result of the getMetadata() function of this handler for a file - * @return Array + * + * @param mixed $metadata Result of the getMetadata() function of this handler for a file + * @return array */ public function getStreamHeaders( $metadata ) { return array(); @@ -264,6 +338,8 @@ abstract class MediaHandler { /** * True if the handled types can be transformed + * + * @param File $file * @return bool */ function canRender( $file ) { @@ -273,6 +349,8 @@ abstract class MediaHandler { /** * True if handled types cannot be displayed directly in a browser * but can be rendered + * + * @param File $file * @return bool */ function mustRender( $file ) { @@ -281,6 +359,8 @@ abstract class MediaHandler { /** * True if the type has multi-page capabilities + * + * @param File $file * @return bool */ function isMultiPage( $file ) { @@ -289,6 +369,8 @@ abstract class MediaHandler { /** * Page count for a multi-page document, false if unsupported or unknown + * + * @param File $file * @return bool */ function pageCount( $file ) { @@ -297,6 +379,8 @@ abstract class MediaHandler { /** * The material is vectorized and thus scaling is lossless + * + * @param File $file * @return bool */ function isVectorized( $file ) { @@ -307,6 +391,8 @@ abstract class MediaHandler { * The material is an image, and is animated. * In particular, video material need not return true. * @note Before 1.20, this was a method of ImageHandler only + * + * @param File $file * @return bool */ function isAnimatedImage( $file ) { @@ -316,6 +402,8 @@ abstract class MediaHandler { /** * If the material is animated, we can animate the thumbnail * @since 1.20 + * + * @param File $file * @return bool If material is not animated, handler may return any value. */ function canAnimateThumbnail( $file ) { @@ -342,8 +430,8 @@ abstract class MediaHandler { * * @note For non-paged media, use getImageSize. * - * @param $image File - * @param $page What page to get dimensions of + * @param File $image + * @param int $page What page to get dimensions of * @return array|bool */ function getPageDimensions( $image, $page ) { @@ -361,13 +449,40 @@ abstract class MediaHandler { /** * Generic getter for text layer. * Currently overloaded by PDF and DjVu handlers - * @return bool + * @param File $image + * @param int $page Page number to get information for + * @return bool|string Page text or false when no text found or if + * unsupported. */ function getPageText( $image, $page ) { return false; } /** + * Get the text of the entire document. + * @param File $file + * @return bool|string The text of the document or false if unsupported. + */ + public function getEntireText( File $file ) { + $numPages = $file->pageCount(); + if ( !$numPages ) { + // Not a multipage document + return $this->getPageText( $file, 1 ); + } + $document = ''; + for ( $i = 1; $i <= $numPages; $i++ ) { + $curPage = $this->getPageText( $file, $i ); + if ( is_string( $curPage ) ) { + $document .= $curPage . "\n"; + } + } + if ( $document !== '' ) { + return $document; + } + return false; + } + + /** * Get an array structure that looks like this: * * array( @@ -387,12 +502,12 @@ abstract class MediaHandler { */ /** - * @todo FIXME: I don't really like this interface, it's not very flexible - * I think the media handler should generate HTML instead. It can do - * all the formatting according to some standard. That makes it possible - * to do things like visual indication of grouped and chained streams - * in ogg container files. - * @return bool + * @todo FIXME: This interface is not very flexible. The media handler + * should generate HTML instead. It can do all the formatting according + * to some standard. That makes it possible to do things like visual + * indication of grouped and chained streams in ogg container files. + * @param File $image + * @return array|bool */ function formatMetadata( $image ) { return false; @@ -404,8 +519,8 @@ abstract class MediaHandler { * * This is used by the media handlers that use the FormatMetadata class * - * @param array $metadataArray metadata array - * @return array for use displaying metadata. + * @param array $metadataArray Metadata array + * @return array Array for use displaying metadata. */ function formatMetadataHelper( $metadataArray ) { $result = array( @@ -425,6 +540,7 @@ abstract class MediaHandler { $value ); } + return $result; } @@ -432,20 +548,10 @@ abstract class MediaHandler { * Get a list of metadata items which should be displayed when * the metadata table is collapsed. * - * @return array of strings - * @access protected + * @return array Array of strings */ - function visibleMetadataFields() { - $fields = array(); - $lines = explode( "\n", wfMessage( 'metadata-fields' )->inContentLanguage()->text() ); - foreach ( $lines as $line ) { - $matches = array(); - if ( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { - $fields[] = $matches[1]; - } - } - $fields = array_map( 'strtolower', $fields ); - return $fields; + protected function visibleMetadataFields() { + return FormatMetadata::getVisibleFields(); } /** @@ -453,21 +559,21 @@ abstract class MediaHandler { * That array is then used to generate the table of metadata values * on the image page * - * @param &$array Array An array containing elements for each type of visibility - * and each of those elements being an array of metadata items. This function adds - * a value to that array. + * @param array &$array An array containing elements for each type of visibility + * and each of those elements being an array of metadata items. This function adds + * a value to that array. * @param string $visibility ('visible' or 'collapsed') if this value is hidden - * by default. - * @param string $type type of metadata tag (currently always 'exif') - * @param string $id the name of the metadata tag (like 'artist' for example). - * its name in the table displayed is the message "$type-$id" (Ex exif-artist ). - * @param string $value thingy goes into a wikitext table; it used to be escaped but - * that was incompatible with previous practise of customized display - * with wikitext formatting via messages such as 'exif-model-value'. - * So the escaping is taken back out, but generally this seems a confusing - * interface. - * @param string $param value to pass to the message for the name of the field - * as $1. Currently this parameter doesn't seem to ever be used. + * by default. + * @param string $type Type of metadata tag (currently always 'exif') + * @param string $id The name of the metadata tag (like 'artist' for example). + * its name in the table displayed is the message "$type-$id" (Ex exif-artist ). + * @param string $value Thingy goes into a wikitext table; it used to be escaped but + * that was incompatible with previous practise of customized display + * with wikitext formatting via messages such as 'exif-model-value'. + * So the escaping is taken back out, but generally this seems a confusing + * interface. + * @param bool|string $param Value to pass to the message for the name of the field + * as $1. Currently this parameter doesn't seem to ever be used. * * Note, everything here is passed through the parser later on (!) */ @@ -492,58 +598,55 @@ abstract class MediaHandler { } /** - * Used instead of getLongDesc if there is no handler registered for file. + * Short description. Shown on Special:Search results. * - * @param $file File + * @param File $file * @return string */ function getShortDesc( $file ) { - global $wgLang; - return htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); + return self::getGeneralShortDesc( $file ); } /** - * Short description. Shown on Special:Search results. + * Long description. Shown under image on image description page surounded by (). * - * @param $file File + * @param File $file * @return string */ function getLongDesc( $file ) { - global $wgLang; - return wfMessage( 'file-info', htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ), - $file->getMimeType() )->parse(); + return self::getGeneralLongDesc( $file ); } /** - * Long description. Shown under image on image description page surounded by (). + * Used instead of getShortDesc if there is no handler registered for file. * - * @param $file File + * @param File $file * @return string */ static function getGeneralShortDesc( $file ) { global $wgLang; - return $wgLang->formatSize( $file->getSize() ); + + return htmlspecialchars( $wgLang->formatSize( $file->getSize() ) ); } /** - * Used instead of getShortDesc if there is no handler registered for file. + * Used instead of getLongDesc if there is no handler registered for file. * - * @param $file File + * @param File $file * @return string */ static function getGeneralLongDesc( $file ) { - global $wgLang; - return wfMessage( 'file-info', $wgLang->formatSize( $file->getSize() ), - $file->getMimeType() )->parse(); + return wfMessage( 'file-info' )->sizeParams( $file->getSize() ) + ->params( $file->getMimeType() )->parse(); } /** * Calculate the largest thumbnail width for a given original file size * such that the thumbnail's height is at most $maxHeight. - * @param $boxWidth Integer Width of the thumbnail box. - * @param $boxHeight Integer Height of the thumbnail box. - * @param $maxHeight Integer Maximum height expected for the thumbnail. - * @return Integer. + * @param int $boxWidth Width of the thumbnail box. + * @param int $boxHeight Height of the thumbnail box. + * @param int $maxHeight Maximum height expected for the thumbnail. + * @return int */ public static function fitBoxWidth( $boxWidth, $boxHeight, $maxHeight ) { $idealWidth = $boxWidth * $maxHeight / $boxHeight; @@ -559,7 +662,7 @@ abstract class MediaHandler { * Shown in file history box on image description page. * * @param File $file - * @return String Dimensions + * @return string Dimensions */ function getDimensionsString( $file ) { return ''; @@ -575,7 +678,8 @@ abstract class MediaHandler { * @param Parser $parser * @param File $file */ - function parserTransformHook( $parser, $file ) {} + function parserTransformHook( $parser, $file ) { + } /** * File validation hook called on upload. @@ -585,7 +689,7 @@ abstract class MediaHandler { * relevant errors. * * @param string $fileName The local path to the file. - * @return Status object + * @return Status */ function verifyUpload( $fileName ) { return Status::newGood(); @@ -614,9 +718,11 @@ abstract class MediaHandler { sprintf( 'Removing bad %d-byte thumbnail "%s". unlink() failed', $thumbstat['size'], $dstPath ) ); } + return true; } } + return false; } @@ -637,12 +743,12 @@ abstract class MediaHandler { // Do nothing } - /* + /** * True if the handler can rotate the media - * @since 1.21 + * @since 1.24 non-static. From 1.21-1.23 was static * @return bool */ - public static function canRotate() { + public function canRotate() { return false; } @@ -657,11 +763,100 @@ abstract class MediaHandler { * * For files we don't know, we return 0. * - * @param $file File + * @param File $file * @return int 0, 90, 180 or 270 */ public function getRotation( $file ) { return 0; } + /** + * Log an error that occurred in an external process + * + * Moved from BitmapHandler to MediaHandler with MediaWiki 1.23 + * + * @since 1.23 + * @param int $retval + * @param string $err Error reported by command. Anything longer than + * MediaHandler::MAX_ERR_LOG_SIZE is stripped off. + * @param string $cmd + */ + protected function logErrorForExternalProcess( $retval, $err, $cmd ) { + # Keep error output limited (bug 57985) + $errMessage = trim( substr( $err, 0, self::MAX_ERR_LOG_SIZE ) ); + + wfDebugLog( 'thumbnail', + sprintf( 'thumbnail failed on %s: error %d "%s" from "%s"', + wfHostname(), $retval, $errMessage, $cmd ) ); + } + + /** + * Get list of languages file can be viewed in. + * + * @param File $file + * @return string[] Array of language codes, or empty array if unsupported. + * @since 1.23 + */ + public function getAvailableLanguages( File $file ) { + return array(); + } + + /** + * On file types that support renderings in multiple languages, + * which language is used by default if unspecified. + * + * If getAvailableLanguages returns a non-empty array, this must return + * a valid language code. Otherwise can return null if files of this + * type do not support alternative language renderings. + * + * @param File $file + * @return string|null Language code or null if multi-language not supported for filetype. + * @since 1.23 + */ + public function getDefaultRenderLanguage( File $file ) { + return null; + } + + /** + * If its an audio file, return the length of the file. Otherwise 0. + * + * File::getLength() existed for a long time, but was calling a method + * that only existed in some subclasses of this class (The TMH ones). + * + * @param File $file + * @return float Length in seconds + * @since 1.23 + */ + public function getLength( $file ) { + return 0.0; + } + + /** + * True if creating thumbnails from the file is large or otherwise resource-intensive. + * @param File $file + * @return bool + */ + public function isExpensiveToThumbnail( $file ) { + return false; + } + + /** + * Returns whether or not this handler supports the chained generation of thumbnails according + * to buckets + * @return bool + * @since 1.24 + */ + public function supportsBucketing() { + return false; + } + + /** + * Returns a normalised params array for which parameters have been cleaned up for bucketing + * purposes + * @param array $params + * @return array + */ + public function sanitizeParamsForBucketing( $params ) { + return $params; + } } diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php index c49d3f20..bc9e9173 100644 --- a/includes/media/MediaTransformOutput.php +++ b/includes/media/MediaTransformOutput.php @@ -27,46 +27,67 @@ * @ingroup Media */ abstract class MediaTransformOutput { - /** - * @var File + /** @var array Associative array mapping optional supplementary image files + * from pixel density (eg 1.5 or 2) to additional URLs. */ - var $file; + public $responsiveUrls = array(); - var $width, $height, $url, $page, $path, $lang; + /** @var File */ + protected $file; - /** - * @var array Associative array mapping optional supplementary image files - * from pixel density (eg 1.5 or 2) to additional URLs. - */ - public $responsiveUrls = array(); + /** @var int Image width */ + protected $width; + + /** @var int Image height */ + protected $height; + + /** @var string URL path to the thumb */ + protected $url; + /** @var bool|string */ + protected $page; + + /** @var bool|string Filesystem path to the thumb */ + protected $path; + + /** @var bool|string Language code, false if not set */ + protected $lang; + + /** @var bool|string Permanent storage path */ protected $storagePath = false; /** - * @return integer Width of the output box + * @return int Width of the output box */ public function getWidth() { return $this->width; } /** - * @return integer Height of the output box + * @return int Height of the output box */ public function getHeight() { return $this->height; } /** + * @return File + */ + public function getFile() { + return $this->file; + } + + /** * Get the final extension of the thumbnail. * Returns false for scripted transformations. - * @return string|false + * @return string|bool */ public function getExtension() { return $this->path ? FileBackend::extensionFromPath( $this->path ) : false; } /** - * @return string|false The thumbnail URL + * @return string|bool The thumbnail URL */ public function getUrl() { return $this->url; @@ -85,6 +106,9 @@ abstract class MediaTransformOutput { */ public function setStoragePath( $storagePath ) { $this->storagePath = $storagePath; + if ( $this->path === false ) { + $this->path = $storagePath; + } } /** @@ -119,11 +143,14 @@ abstract class MediaTransformOutput { /** * Check if an output thumbnail file actually exists. + * * This will return false if there was an error, the * thumbnail is to be handled client-side only, or if * transformation was deferred via TRANSFORM_LATER. + * This file may exist as a new file in /tmp, a file + * in permanent storage, or even refer to the original. * - * @return Bool + * @return bool */ public function hasFile() { // If TRANSFORM_LATER, $this->path will be false. @@ -135,7 +162,7 @@ abstract class MediaTransformOutput { * Check if the output thumbnail is the same as the source. * This can occur if the requested width was bigger than the source. * - * @return Bool + * @return bool */ public function fileIsSource() { return ( !$this->isError() && $this->path === null ); @@ -156,6 +183,7 @@ abstract class MediaTransformOutput { $be = $this->file->getRepo()->getBackend(); // The temp file will be process cached by FileBackend $fsFile = $be->getLocalReference( array( 'src' => $this->path ) ); + return $fsFile ? $fsFile->getPath() : false; } else { return $this->path; // may return false @@ -166,13 +194,14 @@ abstract class MediaTransformOutput { * Stream the file if there were no errors * * @param array $headers Additional HTTP headers to send on success - * @return Bool success + * @return bool Success */ public function streamFile( $headers = array() ) { if ( !$this->path ) { return false; } elseif ( FileBackend::isStoragePath( $this->path ) ) { $be = $this->file->getRepo()->getBackend(); + return $be->streamFile( array( 'src' => $this->path, 'headers' => $headers ) )->isOK(); } else { // FS-file return StreamFile::stream( $this->getLocalCopyPath(), $headers ); @@ -182,9 +211,8 @@ abstract class MediaTransformOutput { /** * Wrap some XHTML text in an anchor tag with the given attributes * - * @param $linkAttribs array - * @param $contents string - * + * @param array $linkAttribs + * @param string $contents * @return string */ protected function linkWrap( $linkAttribs, $contents ) { @@ -196,8 +224,8 @@ abstract class MediaTransformOutput { } /** - * @param $title string - * @param $params string|array Query parameters to add + * @param string $title + * @param string|array $params Query parameters to add * @return array */ public function getDescLinkAttribs( $title = null, $params = array() ) { @@ -224,6 +252,7 @@ abstract class MediaTransformOutput { if ( $title ) { $attribs['title'] = $title; } + return $attribs; } } @@ -241,11 +270,10 @@ class ThumbnailImage extends MediaTransformOutput { * $parameters should include, as a minimum, (file) 'width' and 'height'. * It may also include a 'page' parameter for multipage files. * - * @param $file File object + * @param File $file * @param string $url URL path to the thumb - * @param $path String|bool|null: filesystem path to the thumb + * @param string|bool $path Filesystem path to the thumb * @param array $parameters Associative array of parameters - * @private */ function __construct( $file, $url, $path = false, $parameters = array() ) { # Previous parameters: @@ -300,6 +328,8 @@ class ThumbnailImage extends MediaTransformOutput { * desc-query String, description link query params * override-width Override width attribute. Should generally not set * override-height Override height attribute. Should generally not set + * no-dimensions Boolean, skip width and height attributes (useful if + * set in CSS) * custom-url-link Custom URL to link to * custom-title-link Custom Title object to link to * custom target-link Value of the target attribute, for custom-target-link @@ -336,13 +366,17 @@ class ThumbnailImage extends MediaTransformOutput { $linkAttribs['rel'] = $options['parser-extlink-rel']; } } elseif ( !empty( $options['custom-title-link'] ) ) { + /** @var Title $title */ $title = $options['custom-title-link']; $linkAttribs = array( 'href' => $title->getLinkURL(), 'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title'] ); } elseif ( !empty( $options['desc-link'] ) ) { - $linkAttribs = $this->getDescLinkAttribs( empty( $options['title'] ) ? null : $options['title'], $query ); + $linkAttribs = $this->getDescLinkAttribs( + empty( $options['title'] ) ? null : $options['title'], + $query + ); } elseif ( !empty( $options['file-link'] ) ) { $linkAttribs = array( 'href' => $this->file->getURL() ); } else { @@ -352,9 +386,12 @@ class ThumbnailImage extends MediaTransformOutput { $attribs = array( 'alt' => $alt, 'src' => $this->url, - 'width' => $this->width, - 'height' => $this->height ); + + if ( empty( $options['no-dimensions'] ) ) { + $attribs['width'] = $this->width; + $attribs['height'] = $this->height; + } if ( !empty( $options['valign'] ) ) { $attribs['style'] = "vertical-align: {$options['valign']}"; } @@ -377,7 +414,6 @@ class ThumbnailImage extends MediaTransformOutput { return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); } - } /** @@ -386,7 +422,11 @@ class ThumbnailImage extends MediaTransformOutput { * @ingroup Media */ class MediaTransformError extends MediaTransformOutput { - var $htmlMsg, $textMsg, $width, $height, $url, $path; + /** @var string HTML formatted version of the error */ + private $htmlMsg; + + /** @var string Plain text formatted version of the error */ + private $textMsg; function __construct( $msg, $width, $height /*, ... */ ) { $args = array_slice( func_get_args(), 3 ); diff --git a/includes/media/PNG.php b/includes/media/PNG.php index 98f13861..7b3ddb51 100644 --- a/includes/media/PNG.php +++ b/includes/media/PNG.php @@ -27,7 +27,6 @@ * @ingroup Media */ class PNGHandler extends BitmapHandler { - const BROKEN_FILE = '0'; /** @@ -41,6 +40,7 @@ class PNGHandler extends BitmapHandler { } catch ( Exception $e ) { // Broken file? wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); + return self::BROKEN_FILE; } @@ -48,28 +48,41 @@ class PNGHandler extends BitmapHandler { } /** - * @param $image File + * @param File $image * @return array|bool */ function formatMetadata( $image ) { + $meta = $this->getCommonMetaArray( $image ); + if ( count( $meta ) === 0 ) { + return false; + } + + return $this->formatMetadataHelper( $meta ); + } + + /** + * Get a file type independent array of metadata. + * + * @param File $image + * @return array The metadata array + */ + public function getCommonMetaArray( File $image ) { $meta = $image->getMetadata(); if ( !$meta ) { - return false; + return array(); } $meta = unserialize( $meta ); - if ( !isset( $meta['metadata'] ) || count( $meta['metadata'] ) <= 1 ) { - return false; + if ( !isset( $meta['metadata'] ) ) { + return array(); } + unset( $meta['metadata']['_MW_PNG_VERSION'] ); - if ( isset( $meta['metadata']['_MW_PNG_VERSION'] ) ) { - unset( $meta['metadata']['_MW_PNG_VERSION'] ); - } - return $this->formatMetadataHelper( $meta['metadata'] ); + return $meta['metadata']; } /** - * @param $image File + * @param File $image * @return bool */ function isAnimatedImage( $image ) { @@ -80,12 +93,14 @@ class PNGHandler extends BitmapHandler { return true; } } + return false; } + /** * We do not support making APNG thumbnails, so always false - * @param $image File - * @return bool false + * @param File $image + * @return bool False */ function canAnimateThumbnail( $image ) { return false; @@ -108,19 +123,23 @@ class PNGHandler extends BitmapHandler { if ( !$data || !is_array( $data ) ) { wfDebug( __METHOD__ . " invalid png metadata\n" ); + return self::METADATA_BAD; } if ( !isset( $data['metadata']['_MW_PNG_VERSION'] ) - || $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION ) { + || $data['metadata']['_MW_PNG_VERSION'] != PNGMetadataExtractor::VERSION + ) { wfDebug( __METHOD__ . " old but compatible png metadata\n" ); + return self::METADATA_COMPATIBLE; } + return self::METADATA_GOOD; } /** - * @param $image File + * @param File $image * @return string */ function getLongDesc( $image ) { @@ -155,4 +174,7 @@ class PNGHandler extends BitmapHandler { return $wgLang->commaList( $info ); } + public function supportsBucketing() { + return true; + } } diff --git a/includes/media/PNGMetadataExtractor.php b/includes/media/PNGMetadataExtractor.php index 845d212a..bccd36c1 100644 --- a/includes/media/PNGMetadataExtractor.php +++ b/includes/media/PNGMetadataExtractor.php @@ -31,40 +31,45 @@ * @ingroup Media */ class PNGMetadataExtractor { - static $png_sig; - static $CRC_size; - static $text_chunks; + /** @var string */ + private static $pngSig; + + /** @var int */ + private static $crcSize; + + /** @var array */ + private static $textChunks; const VERSION = 1; const MAX_CHUNK_SIZE = 3145728; // 3 megabytes static function getMetadata( $filename ) { - self::$png_sig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 ); - self::$CRC_size = 4; + self::$pngSig = pack( "C8", 137, 80, 78, 71, 13, 10, 26, 10 ); + self::$crcSize = 4; /* based on list at http://owl.phy.queensu.ca/~phil/exiftool/TagNames/PNG.html#TextualData * and http://www.w3.org/TR/PNG/#11keywords */ - self::$text_chunks = array( + self::$textChunks = array( 'xml:com.adobe.xmp' => 'xmp', # Artist is unofficial. Author is the recommended # keyword in the PNG spec. However some people output # Artist so support both. - 'artist' => 'Artist', - 'model' => 'Model', - 'make' => 'Make', - 'author' => 'Artist', - 'comment' => 'PNGFileComment', + 'artist' => 'Artist', + 'model' => 'Model', + 'make' => 'Make', + 'author' => 'Artist', + 'comment' => 'PNGFileComment', 'description' => 'ImageDescription', - 'title' => 'ObjectName', - 'copyright' => 'Copyright', + 'title' => 'ObjectName', + 'copyright' => 'Copyright', # Source as in original device used to make image # not as in who gave you the image - 'source' => 'Model', - 'software' => 'Software', - 'disclaimer' => 'Disclaimer', - 'warning' => 'ContentWarning', - 'url' => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement. - 'label' => 'Label', + 'source' => 'Model', + 'software' => 'Software', + 'disclaimer' => 'Disclaimer', + 'warning' => 'ContentWarning', + 'url' => 'Identifier', # Not sure if this is best mapping. Maybe WebStatement. + 'label' => 'Label', 'creation time' => 'DateTimeDigitized', /* Other potentially useful things - Document */ ); @@ -90,7 +95,7 @@ class PNGMetadataExtractor { // Check for the PNG header $buf = fread( $fh, 8 ); - if ( $buf != self::$png_sig ) { + if ( $buf != self::$pngSig ) { throw new Exception( __METHOD__ . ": Not a valid PNG file; header: $buf" ); } @@ -181,9 +186,9 @@ class PNGMetadataExtractor { // Theoretically should be case-sensitive, but in practise... $items[1] = strtolower( $items[1] ); - if ( !isset( self::$text_chunks[$items[1]] ) ) { + if ( !isset( self::$textChunks[$items[1]] ) ) { // Only extract textual chunks on our list. - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } @@ -203,26 +208,23 @@ class PNGMetadataExtractor { if ( $items[5] === false ) { // decompression failed wfDebug( __METHOD__ . ' Error decompressing iTxt chunk - ' . $items[1] . "\n" ); - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } - } else { wfDebug( __METHOD__ . ' Skipping compressed png iTXt chunk due to lack of zlib,' . " or potentially invalid compression method\n" ); - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } } - $finalKeyword = self::$text_chunks[$items[1]]; + $finalKeyword = self::$textChunks[$items[1]]; $text[$finalKeyword][$items[3]] = $items[5]; $text[$finalKeyword]['_type'] = 'lang'; - } else { // Error reading iTXt chunk throw new Exception( __METHOD__ . ": Read error on iTXt chunk" ); } - } elseif ( $chunk_type == 'tEXt' ) { $buf = self::read( $fh, $chunk_size ); @@ -238,9 +240,9 @@ class PNGMetadataExtractor { // Theoretically should be case-sensitive, but in practise... $keyword = strtolower( $keyword ); - if ( !isset( self::$text_chunks[ $keyword ] ) ) { + if ( !isset( self::$textChunks[$keyword] ) ) { // Don't recognize chunk, so skip. - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } wfSuppressWarnings(); @@ -251,10 +253,9 @@ class PNGMetadataExtractor { throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); } - $finalKeyword = self::$text_chunks[$keyword]; + $finalKeyword = self::$textChunks[$keyword]; $text[$finalKeyword]['x-default'] = $content; $text[$finalKeyword]['_type'] = 'lang'; - } elseif ( $chunk_type == 'zTXt' ) { if ( function_exists( 'gzuncompress' ) ) { $buf = self::read( $fh, $chunk_size ); @@ -271,16 +272,16 @@ class PNGMetadataExtractor { // Theoretically should be case-sensitive, but in practise... $keyword = strtolower( $keyword ); - if ( !isset( self::$text_chunks[ $keyword ] ) ) { + if ( !isset( self::$textChunks[$keyword] ) ) { // Don't recognize chunk, so skip. - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } $compression = substr( $postKeyword, 0, 1 ); $content = substr( $postKeyword, 1 ); if ( $compression !== "\x00" ) { wfDebug( __METHOD__ . " Unrecognized compression method in zTXt ($keyword). Skipping.\n" ); - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } @@ -291,7 +292,7 @@ class PNGMetadataExtractor { if ( $content === false ) { // decompression failed wfDebug( __METHOD__ . ' Error decompressing zTXt chunk - ' . $keyword . "\n" ); - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); continue; } @@ -303,10 +304,9 @@ class PNGMetadataExtractor { throw new Exception( __METHOD__ . ": Read error (error with iconv)" ); } - $finalKeyword = self::$text_chunks[$keyword]; + $finalKeyword = self::$textChunks[$keyword]; $text[$finalKeyword]['x-default'] = $content; $text[$finalKeyword]['_type'] = 'lang'; - } else { wfDebug( __METHOD__ . " Cannot decompress zTXt chunk due to lack of zlib. Skipping.\n" ); fseek( $fh, $chunk_size, SEEK_CUR ); @@ -332,7 +332,6 @@ class PNGMetadataExtractor { if ( $exifTime ) { $text['DateTime'] = $exifTime; } - } elseif ( $chunk_type == 'pHYs' ) { // how big pixels are (dots per meter). if ( $chunk_size !== 9 ) { @@ -359,13 +358,12 @@ class PNGMetadataExtractor { // 3 = dots per cm (from Exif). } } - } elseif ( $chunk_type == "IEND" ) { break; } else { fseek( $fh, $chunk_size, SEEK_CUR ); } - fseek( $fh, self::$CRC_size, SEEK_CUR ); + fseek( $fh, self::$crcSize, SEEK_CUR ); } fclose( $fh ); @@ -399,6 +397,7 @@ class PNGMetadataExtractor { } } } + return array( 'frameCount' => $frameCount, 'loopCount' => $loopCount, @@ -407,21 +406,22 @@ class PNGMetadataExtractor { 'bitDepth' => $bitDepth, 'colorType' => $colorType, ); - } + /** * Read a chunk, checking to make sure its not too big. * - * @param $fh resource The file handle - * @param $size Integer size in bytes. - * @throws Exception if too big. - * @return String The chunk. + * @param resource $fh The file handle + * @param int $size Size in bytes. + * @throws Exception If too big + * @return string The chunk. */ private static function read( $fh, $size ) { if ( $size > self::MAX_CHUNK_SIZE ) { throw new Exception( __METHOD__ . ': Chunk size of ' . $size . ' too big. Max size is: ' . self::MAX_CHUNK_SIZE ); } + return fread( $fh, $size ); } } diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 72a9696c..74e5e048 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -29,10 +29,22 @@ class SvgHandler extends ImageHandler { const SVG_METADATA_VERSION = 2; + /** @var array A list of metadata tags that can be converted + * to the commonly used exif tags. This allows messages + * to be reused, and consistent tag names for {{#formatmetadata:..}} + */ + private static $metaConversion = array( + 'originalwidth' => 'ImageWidth', + 'originalheight' => 'ImageLength', + 'description' => 'ImageDescription', + 'title' => 'ObjectName', + ); + function isEnabled() { global $wgSVGConverters, $wgSVGConverter; if ( !isset( $wgSVGConverters[$wgSVGConverter] ) ) { wfDebug( "\$wgSVGConverter is invalid, disabling SVG rendering.\n" ); + return false; } else { return true; @@ -48,11 +60,11 @@ class SvgHandler extends ImageHandler { } /** - * @param $file File + * @param File $file * @return bool */ function isAnimatedImage( $file ) { - # TODO: detect animated SVGs + # @todo Detect animated SVGs $metadata = $file->getMetadata(); if ( $metadata ) { $metadata = $this->unpackMetadata( $metadata ); @@ -60,19 +72,60 @@ class SvgHandler extends ImageHandler { return $metadata['animated']; } } + return false; } /** + * Which languages (systemLanguage attribute) is supported. + * + * @note This list is not guaranteed to be exhaustive. + * To avoid OOM errors, we only look at first bit of a file. + * Thus all languages on this list are present in the file, + * but its possible for the file to have a language not on + * this list. + * + * @param File $file + * @return array Array of language codes, or empty if no language switching supported. + */ + public function getAvailableLanguages( File $file ) { + $metadata = $file->getMetadata(); + $langList = array(); + if ( $metadata ) { + $metadata = $this->unpackMetadata( $metadata ); + if ( isset( $metadata['translations'] ) ) { + foreach ( $metadata['translations'] as $lang => $langType ) { + if ( $langType === SvgReader::LANG_FULL_MATCH ) { + $langList[] = $lang; + } + } + } + } + return $langList; + } + + /** + * What language to render file in if none selected. + * + * @param File $file + * @return string Language code. + */ + public function getDefaultRenderLanguage( File $file ) { + return 'en'; + } + + /** * We do not support making animated svg thumbnails + * @param File $file + * @return bool */ - function canAnimateThumb( $file ) { + function canAnimateThumbnail( $file ) { return false; } /** - * @param $image File - * @param $params + * @param File $image + * @param array $params * @return bool */ function normaliseParams( $image, &$params ) { @@ -96,14 +149,15 @@ class SvgHandler extends ImageHandler { $params['physicalHeight'] = $wgSVGMaxSize; } } + return true; } /** - * @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 bool|MediaTransformError|ThumbnailImage|TransformParameterError */ @@ -115,7 +169,7 @@ class SvgHandler extends ImageHandler { $clientHeight = $params['height']; $physicalWidth = $params['physicalWidth']; $physicalHeight = $params['physicalHeight']; - $lang = isset( $params['lang'] ) ? $params['lang'] : 'en'; + $lang = isset( $params['lang'] ) ? $params['lang'] : $this->getDefaultRenderLanguage( $image ); if ( $flags & self::TRANSFORM_LATER ) { return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); @@ -124,6 +178,7 @@ class SvgHandler extends ImageHandler { $metadata = $this->unpackMetadata( $image->getMetadata() ); if ( isset( $metadata['error'] ) ) { // sanity check $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text(); + return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); } @@ -133,7 +188,40 @@ class SvgHandler extends ImageHandler { } $srcPath = $image->getLocalRefPath(); - $status = $this->rasterize( $srcPath, $dstPath, $physicalWidth, $physicalHeight, $lang ); + 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() + ); + } + + // Make a temp dir with a symlink to the local copy in it. + // This plays well with rsvg-convert policy for external entities. + // https://git.gnome.org/browse/librsvg/commit/?id=f01aded72c38f0e18bc7ff67dee800e380251c8e + $tmpDir = wfTempDir() . '/svg_' . wfRandomString( 24 ); + $lnPath = "$tmpDir/" . basename( $srcPath ); + $ok = mkdir( $tmpDir, 0771 ) && symlink( $srcPath, $lnPath ); + $cleaner = new ScopedCallback( function () use ( $tmpDir, $lnPath ) { + wfSuppressWarnings(); + unlink( $lnPath ); + rmdir( $tmpDir ); + wfRestoreWarnings(); + } ); + if ( !$ok ) { + wfDebugLog( 'thumbnail', + sprintf( 'Thumbnail failed on %s: could not link %s to %s', + wfHostname(), $lnPath, $srcPath ) ); + return new MediaTransformError( 'thumbnail_error', + $params['width'], $params['height'], + wfMessage( 'thumbnail-temp-create' )->text() + ); + } + + $status = $this->rasterize( $lnPath, $dstPath, $physicalWidth, $physicalHeight, $lang ); if ( $status === true ) { return new ThumbnailImage( $image, $dstUrl, $dstPath, $params ); } else { @@ -148,7 +236,7 @@ class SvgHandler extends ImageHandler { * @param string $dstPath * @param string $width * @param string $height - * @param string $lang Language code of the language to render the SVG in + * @param bool|string $lang Language code of the language to render the SVG in * @throws MWException * @return bool|MediaTransformError */ @@ -192,10 +280,10 @@ class SvgHandler 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 ); } + return true; } @@ -214,9 +302,9 @@ class SvgHandler extends ImageHandler { } /** - * @param $file File - * @param $path - * @param bool $metadata + * @param File $file + * @param string $path Unused + * @param bool|array $metadata * @return array */ function getImageSize( $file, $path, $metadata = false ) { @@ -227,7 +315,7 @@ class SvgHandler extends ImageHandler { if ( isset( $metadata['width'] ) && isset( $metadata['height'] ) ) { return array( $metadata['width'], $metadata['height'], 'SVG', - "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ); + "width=\"{$metadata['width']}\" height=\"{$metadata['height']}\"" ); } else { // error return array( 0, 0, 'SVG', "width=\"0\" height=\"0\"" ); } @@ -243,7 +331,7 @@ class SvgHandler extends ImageHandler { * a "nominal" resolution, and not a fixed one, * as well as so animation can be denoted. * - * @param $file File + * @param File $file * @return string */ function getLongDesc( $file ) { @@ -267,6 +355,11 @@ class SvgHandler extends ImageHandler { return $msg->parse(); } + /** + * @param File $file + * @param string $filename + * @return string Serialised metadata + */ function getMetadata( $file, $filename ) { $metadata = array( 'version' => self::SVG_METADATA_VERSION ); try { @@ -279,6 +372,7 @@ class SvgHandler extends ImageHandler { ); wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); } + return serialize( $metadata ); } @@ -306,16 +400,18 @@ class SvgHandler extends ImageHandler { // Old but compatible return self::METADATA_COMPATIBLE; } + return self::METADATA_GOOD; } - function visibleMetadataFields() { + protected function visibleMetadataFields() { $fields = array( 'objectname', 'imagedescription' ); + return $fields; } /** - * @param $file File + * @param File $file * @return array|bool */ function formatMetadata( $file ) { @@ -332,7 +428,7 @@ class SvgHandler extends ImageHandler { return false; } - /* TODO: add a formatter + /* @todo Add a formatter $format = new FormatSVG( $metadata ); $formatted = $format->getFormattedData(); */ @@ -340,19 +436,11 @@ class SvgHandler extends ImageHandler { // Sort fields into visible and collapsed $visibleFields = $this->visibleMetadataFields(); - // Rename fields to be compatible with exif, so that - // the labels for these fields work and reuse existing messages. - $conversion = array( - 'originalwidth' => 'imagewidth', - 'originalheight' => 'imagelength', - 'description' => 'imagedescription', - 'title' => 'objectname', - ); $showMeta = false; foreach ( $metadata as $name => $value ) { $tag = strtolower( $name ); - if ( isset( $conversion[$tag] ) ) { - $tag = $conversion[$tag]; + if ( isset( self::$metaConversion[$tag] ) ) { + $tag = strtolower( self::$metaConversion[$tag] ); } else { // Do not output other metadata not in list continue; @@ -365,13 +453,13 @@ class SvgHandler extends ImageHandler { $value ); } + return $showMeta ? $result : false; } - /** * @param string $name Parameter name - * @param $string $value Parameter value + * @param mixed $value Parameter value * @return bool Validity */ function validateParam( $name, $value ) { @@ -380,18 +468,21 @@ class SvgHandler extends ImageHandler { return ( $value > 0 ); } elseif ( $name == 'lang' ) { // Validate $code - if ( !Language::isValidBuiltinCode( $value ) ) { + if ( $value === '' || !Language::isValidBuiltinCode( $value ) ) { wfDebug( "Invalid user language code\n" ); + return false; } + return true; } + // Only lang, width and height are acceptable keys return false; } /** - * @param array $params name=>value pairs of parameters + * @param array $params Name=>value pairs of parameters * @return string Filename to use */ function makeParamString( $params ) { @@ -403,6 +494,7 @@ class SvgHandler extends ImageHandler { if ( !isset( $params['width'] ) ) { return false; } + return "$lang{$params['width']}px"; } @@ -422,13 +514,41 @@ class SvgHandler extends ImageHandler { } /** - * @param $params + * @param array $params * @return array */ function getScriptParams( $params ) { - return array( - 'width' => $params['width'], - 'lang' => $params['lang'], - ); + $scriptParams = array( 'width' => $params['width'] ); + if ( isset( $params['lang'] ) ) { + $scriptParams['lang'] = $params['lang']; + } + + return $scriptParams; + } + + public function getCommonMetaArray( File $file ) { + $metadata = $file->getMetadata(); + if ( !$metadata ) { + return array(); + } + $metadata = $this->unpackMetadata( $metadata ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return array(); + } + $stdMetadata = array(); + foreach ( $metadata as $name => $value ) { + $tag = strtolower( $name ); + if ( $tag === 'originalwidth' || $tag === 'originalheight' ) { + // Skip these. In the exif metadata stuff, it is assumed these + // are measured in px, which is not the case here. + continue; + } + if ( isset( self::$metaConversion[$tag] ) ) { + $tag = self::$metaConversion[$tag]; + $stdMetadata[$tag] = $value; + } + } + + return $stdMetadata; } } diff --git a/includes/media/SVGMetadataExtractor.php b/includes/media/SVGMetadataExtractor.php index 2e33bb98..2a1091d8 100644 --- a/includes/media/SVGMetadataExtractor.php +++ b/includes/media/SVGMetadataExtractor.php @@ -31,6 +31,7 @@ class SVGMetadataExtractor { static function getMetadata( $filename ) { $svg = new SVGReader( $filename ); + return $svg->getMetadata(); } } @@ -42,10 +43,19 @@ class SVGReader { const DEFAULT_WIDTH = 512; const DEFAULT_HEIGHT = 512; const NS_SVG = 'http://www.w3.org/2000/svg'; + const LANG_PREFIX_MATCH = 1; + const LANG_FULL_MATCH = 2; + /** @var null|XMLReader */ private $reader = null; + + /** @var bool */ private $mDebug = false; - private $metadata = Array(); + + /** @var array */ + private $metadata = array(); + private $languages = array(); + private $languagePrefixes = array(); /** * Constructor @@ -113,7 +123,7 @@ class SVGReader { } /** - * @return Array with the known metadata + * @return array Array with the known metadata */ public function getMetadata() { return $this->metadata; @@ -148,7 +158,9 @@ class SVGReader { $this->debug( "$tag" ); - if ( $isSVG && $tag == 'svg' && $type == XmlReader::END_ELEMENT && $this->reader->depth <= $exitDepth ) { + if ( $isSVG && $tag == 'svg' && $type == XmlReader::END_ELEMENT + && $this->reader->depth <= $exitDepth + ) { break; } elseif ( $isSVG && $tag == 'title' ) { $this->readField( $tag, 'title' ); @@ -164,10 +176,8 @@ class SVGReader { } elseif ( $tag !== '#text' ) { $this->debug( "Unhandled top-level XML tag $tag" ); - if ( !isset( $this->metadata['animated'] ) ) { - // Recurse into children of current tag, looking for animation. - $this->animateFilter( $tag ); - } + // Recurse into children of current tag, looking for animation and languages. + $this->animateFilterAndLang( $tag ); } // Goto next element, which is sibling of current (Skip children). @@ -176,14 +186,16 @@ class SVGReader { $this->reader->close(); + $this->metadata['translations'] = $this->languages + $this->languagePrefixes; + return true; } /** * Read a textelement from an element * - * @param string $name of the element that we are reading from - * @param string $metafield that we will fill with the result + * @param string $name Name of the element that we are reading from + * @param string $metafield Field that we will fill with the result */ private function readField( $name, $metafield = null ) { $this->debug( "Read field $metafield" ); @@ -192,7 +204,10 @@ class SVGReader { } $keepReading = $this->reader->read(); while ( $keepReading ) { - if ( $this->reader->localName == $name && $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::END_ELEMENT ) { + if ( $this->reader->localName == $name + && $this->reader->namespaceURI == self::NS_SVG + && $this->reader->nodeType == XmlReader::END_ELEMENT + ) { break; } elseif ( $this->reader->nodeType == XmlReader::TEXT ) { $this->metadata[$metafield] = trim( $this->reader->value ); @@ -204,7 +219,7 @@ class SVGReader { /** * Read an XML snippet from an element * - * @param string $metafield that we will fill with the result + * @param string $metafield Field that we will fill with the result * @throws MWException */ private function readXml( $metafield = null ) { @@ -212,21 +227,24 @@ class SVGReader { if ( !$metafield || $this->reader->nodeType != XmlReader::ELEMENT ) { return; } - // TODO: find and store type of xml snippet. metadata['metadataType'] = "rdf" + // @todo Find and store type of xml snippet. metadata['metadataType'] = "rdf" if ( method_exists( $this->reader, 'readInnerXML' ) ) { $this->metadata[$metafield] = trim( $this->reader->readInnerXML() ); } else { - throw new MWException( "The PHP XMLReader extension does not come with readInnerXML() method. Your libxml is probably out of date (need 2.6.20 or later)." ); + throw new MWException( "The PHP XMLReader extension does not come " . + "with readInnerXML() method. Your libxml is probably out of " . + "date (need 2.6.20 or later)." ); } $this->reader->next(); } /** - * Filter all children, looking for animate elements + * Filter all children, looking for animated elements. + * Also get a list of languages that can be targeted. * - * @param string $name of the element that we are reading from + * @param string $name Name of the element that we are reading from */ - private function animateFilter( $name ) { + private function animateFilterAndLang( $name ) { $this->debug( "animate filter for tag $name" ); if ( $this->reader->nodeType != XmlReader::ELEMENT ) { return; @@ -238,9 +256,38 @@ class SVGReader { $keepReading = $this->reader->read(); while ( $keepReading ) { if ( $this->reader->localName == $name && $this->reader->depth <= $exitDepth - && $this->reader->nodeType == XmlReader::END_ELEMENT ) { + && $this->reader->nodeType == XmlReader::END_ELEMENT + ) { break; - } elseif ( $this->reader->namespaceURI == self::NS_SVG && $this->reader->nodeType == XmlReader::ELEMENT ) { + } elseif ( $this->reader->namespaceURI == self::NS_SVG + && $this->reader->nodeType == XmlReader::ELEMENT + ) { + + $sysLang = $this->reader->getAttribute( 'systemLanguage' ); + if ( !is_null( $sysLang ) && $sysLang !== '' ) { + // See http://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute + $langList = explode( ',', $sysLang ); + foreach ( $langList as $langItem ) { + $langItem = trim( $langItem ); + if ( Language::isWellFormedLanguageTag( $langItem ) ) { + $this->languages[$langItem] = self::LANG_FULL_MATCH; + } + // Note, the standard says that any prefix should work, + // here we do only the initial prefix, since that will catch + // 99% of cases, and we are going to compare against fallbacks. + // This differs mildly from how the spec says languages should be + // handled, however it matches better how the MediaWiki language + // preference is generally handled. + $dash = strpos( $langItem, '-' ); + // Intentionally checking both !false and > 0 at the same time. + if ( $dash ) { + $itemPrefix = substr( $langItem, 0, $dash ); + if ( Language::isWellFormedLanguageTag( $itemPrefix ) ) { + $this->languagePrefixes[$itemPrefix] = self::LANG_PREFIX_MATCH; + } + } + } + } switch ( $this->reader->localName ) { case 'script': // Normally we disallow files with @@ -261,6 +308,7 @@ class SVGReader { } } + // @todo FIXME: Unused, remove? private function throwXmlError( $err ) { $this->debug( "FAILURE: $err" ); wfDebug( "SVGReader XML error: $err\n" ); @@ -272,10 +320,12 @@ class SVGReader { } } + // @todo FIXME: Unused, remove? private function warn( $data ) { wfDebug( "SVGReader: $data\n" ); } + // @todo FIXME: Unused, remove? private function notice( $data ) { wfDebug( "SVGReader WARN: $data\n" ); } @@ -333,8 +383,8 @@ class SVGReader { * http://www.w3.org/TR/SVG11/coords.html#UnitIdentifiers * * @param string $length CSS/SVG length. - * @param $viewportSize: Float optional scale for percentage units... - * @return float: length in pixels + * @param float|int $viewportSize Optional scale for percentage units... + * @return float Length in pixels */ static function scaleSVGUnit( $length, $viewportSize = 512 ) { static $unitLength = array( @@ -347,7 +397,7 @@ class SVGReader { 'em' => 16.0, // fake it? 'ex' => 12.0, // fake it? '' => 1.0, // "User units" pixels by default - ); + ); $matches = array(); if ( preg_match( '/^\s*(\d+(?:\.\d+)?)(em|ex|px|pt|pc|cm|mm|in|%|)\s*$/', $length, $matches ) ) { $length = floatval( $matches[1] ); diff --git a/includes/media/Tiff.php b/includes/media/Tiff.php index 55acb120..bea6cab3 100644 --- a/includes/media/Tiff.php +++ b/includes/media/Tiff.php @@ -27,6 +27,7 @@ * @ingroup Media */ class TiffHandler extends ExifBitmapHandler { + const EXPENSIVE_SIZE_LIMIT = 10485760; // TIFF files over 10M are considered expensive to thumbnail /** * Conversion to PNG for inline display can be disabled here... @@ -36,12 +37,12 @@ class TiffHandler extends ExifBitmapHandler { * InstantCommons will have thumbnails managed from the remote instance, * so we can skip this check. * - * @param $file - * + * @param File $file * @return bool */ function canRender( $file ) { global $wgTiffThumbnailType; + return (bool)$wgTiffThumbnailType || $file->getRepo() instanceof ForeignAPIRepo; } @@ -50,8 +51,7 @@ class TiffHandler extends ExifBitmapHandler { * Browsers don't support TIFF inline generally... * For inline display, we need to convert to PNG. * - * @param $file - * + * @param File $file * @return bool */ function mustRender( $file ) { @@ -59,13 +59,14 @@ class TiffHandler extends ExifBitmapHandler { } /** - * @param $ext - * @param $mime - * @param $params + * @param string $ext + * @param string $mime + * @param array $params * @return bool */ function getThumbType( $ext, $mime, $params = null ) { global $wgTiffThumbnailType; + return $wgTiffThumbnailType; } @@ -85,16 +86,21 @@ class TiffHandler extends ExifBitmapHandler { throw new MWException( 'Metadata array is not an array' ); } $meta['MEDIAWIKI_EXIF_VERSION'] = Exif::version(); + return serialize( $meta ); - } - catch ( MWException $e ) { + } catch ( MWException $e ) { // BitmapMetadataHandler throws an exception in certain exceptional // cases like if file does not exist. wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" ); + return ExifBitmapHandler::BROKEN_FILE; } } else { return ''; } } + + public function isExpensiveToThumbnail( $file ) { + return $file->getSize() > static::EXPENSIVE_SIZE_LIMIT; + } } diff --git a/includes/media/TransformationalImageHandler.php b/includes/media/TransformationalImageHandler.php new file mode 100644 index 00000000..3e3be3d1 --- /dev/null +++ b/includes/media/TransformationalImageHandler.php @@ -0,0 +1,593 @@ +<?php +/** + * Base class for handlers which require transforming images in a + * similar way as BitmapHandler does. + * + * This was split from BitmapHandler on the basis that some extensions + * might want to work in a similar way to BitmapHandler, but for + * different formats. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Media + */ + +/** + * Handler for images that need to be transformed + * + * @since 1.24 + * @ingroup Media + */ +abstract class TransformationalImageHandler extends ImageHandler { + /** + * @param File $image + * @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 + # For historical reasons, hook starts with BitmapHandler + $checkImageAreaHookResult = null; + wfRunHooks( + 'BitmapHandlerCheckImageArea', + array( $image, &$params, &$checkImageAreaHookResult ) + ); + + if ( is_null( $checkImageAreaHookResult ) ) { + global $wgMaxImageArea; + + if ( $srcWidth * $srcHeight > $wgMaxImageArea + && !( $image->getMimeType() == 'image/jpeg' + && $this->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 ); + } + + /** + * Create a thumbnail. + * + * This sets up various parameters, and then calls a helper method + * based on $this->getScalerType in order to scale the image. + * + * @param File $image + * @param string $dstPath + * @param string $dstUrl + * @param array $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, + ); + + if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) { + $scalerParams['quality'] = 30; + } + + // For subclasses that might be paged. + if ( $image->isMultipage() && isset( $params['page'] ) ) { + $scalerParams['page'] = intval( $params['page'] ); + } + + # Determine scaler type + $scaler = $this->getScalerType( $dstPath ); + + if ( is_array( $scaler ) ) { + $scalerName = get_class( $scaler[0] ); + } else { + $scalerName = $scaler; + } + + wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " . + "thumbnail at $dstPath using scaler $scalerName\n" ); + + if ( !$image->mustRender() && + $scalerParams['physicalWidth'] == $scalerParams['srcWidth'] + && $scalerParams['physicalHeight'] == $scalerParams['srcHeight'] + && !isset( $scalerParams['quality'] ) + ) { + + # 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" ); + $newParams = array( + 'width' => $scalerParams['clientWidth'], + 'height' => $scalerParams['clientHeight'] + ); + if ( isset( $params['quality'] ) ) { + $newParams['quality'] = $params['quality']; + } + if ( isset( $params['page'] ) && $params['page'] ) { + $newParams['page'] = $params['page']; + } + return new ThumbnailImage( $image, $dstUrl, false, $newParams ); + } + + # 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 + $thumbnailSource = $this->getThumbnailSource( $image, $params ); + + $scalerParams['srcPath'] = $thumbnailSource['path']; + $scalerParams['srcWidth'] = $thumbnailSource['width']; + $scalerParams['srcHeight'] = $thumbnailSource['height']; + + if ( $scalerParams['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', + $scalerParams['clientWidth'], $scalerParams['clientHeight'], + wfMessage( 'filemissing' )->text() + ); + } + + # Try a hook. Called "Bitmap" for historical reasons. + /** @var $mto MediaTransformOutput */ + $mto = null; + wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) ); + if ( !is_null( $mto ) ) { + wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" ); + $scaler = 'hookaborted'; + } + + // $scaler will return a MediaTransformError on failure, or false on success. + // If the scaler is succesful, it will have created a thumbnail at the destination + // path. + if ( is_array( $scaler ) && is_callable( $scaler ) ) { + // Allow subclasses to specify their own rendering methods. + $err = call_user_func( $scaler, $image, $scalerParams ); + } else { + 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'], + wfMessage( 'unknown-error' )->text() + ); + } elseif ( $mto ) { + return $mto; + } else { + $newParams = array( + 'width' => $scalerParams['clientWidth'], + 'height' => $scalerParams['clientHeight'] + ); + if ( isset( $params['quality'] ) ) { + $newParams['quality'] = $params['quality']; + } + if ( isset( $params['page'] ) && $params['page'] ) { + $newParams['page'] = $params['page']; + } + return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams ); + } + } + + /** + * Get the source file for the transform + * + * @param $file File + * @param $params Array + * @return Array Array with keys width, height and path. + */ + protected function getThumbnailSource( $file, $params ) { + return $file->getThumbnailSource( $params ); + } + + /** + * Returns what sort of scaler type should be used. + * + * Values can be one of client, im, custom, gd, imext, or an array + * of object, method-name to call that specific method. + * + * If specifying a custom scaler command with array( Obj, method ), + * the method in question should take 2 parameters, a File object, + * and a $scalerParams array with various options (See doTransform + * for what is in $scalerParams). On error it should return a + * MediaTransformError object. On success it should return false, + * and simply make sure the thumbnail file is located at + * $scalerParams['dstPath']. + * + * If there is a problem with the output path, it returns "client" + * to do client side scaling. + * + * @param string $dstPath + * @param bool $checkDstPath Check that $dstPath is valid + * @return string|Callable One of client, im, custom, gd, imext, or a Callable array. + */ + abstract protected function getScalerType( $dstPath, $checkDstPath = true ); + + /** + * Get a ThumbnailImage that respresents an image that will be scaled + * client side + * + * @param File $image 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 ); + } + + /** + * Transform an image using ImageMagick + * + * This is a stub method. The real method is in BitmapHander. + * + * @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 ) { + return $this->getMediaTransformError( $params, "Unimplemented" ); + } + + /** + * Transform an image using the Imagick PHP extension + * + * This is a stub method. The real method is in BitmapHander. + * + * @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 transformImageMagickExt( $image, $params ) { + return $this->getMediaTransformError( $params, "Unimplemented" ); + } + + /** + * Transform an image using a custom command + * + * This is a stub method. The real method is in BitmapHander. + * + * @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 transformCustom( $image, $params ) { + return $this->getMediaTransformError( $params, "Unimplemented" ); + } + + /** + * 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 ); + } + + /** + * Transform an image using the built in GD library + * + * This is a stub method. The real method is in BitmapHander. + * + * @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 transformGd( $image, $params ) { + return $this->getMediaTransformError( $params, "Unimplemented" ); + } + + /** + * Escape a string for ImageMagick's property input (e.g. -set -comment) + * See InterpretImageProperties() in magick/property.c + * @param string $s + * @return 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. + * @param string $path The file path + * @param bool|string $scene The scene specification, or false if there is none + * @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. + */ + 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; + } + + /** + * Returns whether the current scaler supports rotation. + * + * @since 1.24 No longer static + * @return bool + */ + public function canRotate() { + return false; + } + + /** + * Should we automatically rotate an image based on exif + * + * @since 1.24 No longer static + * @see $wgEnableAutoRotation + * @return bool Whether auto rotation is enabled + */ + public function autoRotateEnabled() { + return false; + } + + /** + * Rotate a thumbnail. + * + * This is a stub. See BitmapHandler::rotate. + * + * @param File $file + * @param array $params Rotate parameters. + * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 + * @since 1.24 Is non-static. From 1.21 it was static + * @return bool + */ + public function rotate( $file, $params ) { + return new MediaTransformError( 'thumbnail_error', 0, 0, + get_class( $this ) . ' rotation not implemented' ); + } + + /** + * Returns 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 $this->canRotate() && $this->getRotation( $file ) != 0; + } +} diff --git a/includes/media/XCF.php b/includes/media/XCF.php index e77d3842..48b7a47c 100644 --- a/includes/media/XCF.php +++ b/includes/media/XCF.php @@ -33,9 +33,8 @@ * @ingroup Media */ class XCFHandler extends BitmapHandler { - /** - * @param $file + * @param File $file * @return bool */ function mustRender( $file ) { @@ -45,9 +44,9 @@ class XCFHandler extends BitmapHandler { /** * Render files as PNG * - * @param $ext - * @param $mime - * @param $params + * @param string $ext + * @param string $mime + * @param array $params * @return array */ function getThumbType( $ext, $mime, $params = null ) { @@ -57,12 +56,33 @@ class XCFHandler extends BitmapHandler { /** * Get width and height from the XCF header. * - * @param $image - * @param $filename + * @param File $image + * @param string $filename * @return array */ function getImageSize( $image, $filename ) { - return self::getXCFMetaData( $filename ); + $header = self::getXCFMetaData( $filename ); + if ( !$header ) { + return false; + } + + # Forge a return array containing metadata information just like getimagesize() + # See PHP documentation at: http://www.php.net/getimagesize + $metadata = array(); + $metadata[0] = $header['width']; + $metadata[1] = $header['height']; + $metadata[2] = null; # IMAGETYPE constant, none exist for XCF. + $metadata[3] = sprintf( + 'height="%s" width="%s"', $header['height'], $header['width'] + ); + $metadata['mime'] = 'image/x-xcf'; + $metadata['channels'] = null; + $metadata['bits'] = 8; # Always 8-bits per color + + assert( '7 == count($metadata); ' . + '# return array must contains 7 elements just like getimagesize() return' ); + + return $metadata; } /** @@ -73,7 +93,7 @@ class XCFHandler extends BitmapHandler { * @author Hashar * * @param string $filename Full path to a XCF file - * @return bool|array metadata array just like PHP getimagesize() + * @return bool|array Metadata Array just like PHP getimagesize() */ static function getXCFMetaData( $filename ) { # Decode master structure @@ -103,12 +123,12 @@ class XCFHandler extends BitmapHandler { # (enum GimpImageBaseType in libgimpbase/gimpbaseenums.h) try { $header = wfUnpack( - "A9magic" # A: space padded - . "/a5version" # a: zero padded - . "/Nwidth" # \ - . "/Nheight" # N: unsigned long 32bit big endian - . "/Nbase_type" # / - , $binaryHeader + "A9magic" . # A: space padded + "/a5version" . # a: zero padded + "/Nwidth" . # \ + "/Nheight" . # N: unsigned long 32bit big endian + "/Nbase_type", # / + $binaryHeader ); } catch ( MWException $mwe ) { return false; @@ -117,36 +137,97 @@ class XCFHandler extends BitmapHandler { # Check values if ( $header['magic'] !== 'gimp xcf' ) { wfDebug( __METHOD__ . " '$filename' has invalid magic signature.\n" ); + return false; } # TODO: we might want to check for sane values of width and height - wfDebug( __METHOD__ . ": canvas size of '$filename' is {$header['width']} x {$header['height']} px\n" ); + wfDebug( __METHOD__ . + ": canvas size of '$filename' is {$header['width']} x {$header['height']} px\n" ); - # Forge a return array containing metadata information just like getimagesize() - # See PHP documentation at: http://www.php.net/getimagesize + return $header; + } + + /** + * Store the channel type + * + * Greyscale files need different command line options. + * + * @param File $file The image object, or false if there isn't one. + * Warning, FSFile::getPropsFromPath might pass an (object)array() instead (!) + * @param string $filename The filename + * @return string + */ + public function getMetadata( $file, $filename ) { + $header = self::getXCFMetadata( $filename ); $metadata = array(); - $metadata[0] = $header['width']; - $metadata[1] = $header['height']; - $metadata[2] = null; # IMAGETYPE constant, none exist for XCF. - $metadata[3] = sprintf( - 'height="%s" width="%s"', $header['height'], $header['width'] - ); - $metadata['mime'] = 'image/x-xcf'; - $metadata['channels'] = null; - $metadata['bits'] = 8; # Always 8-bits per color + if ( $header ) { + // Try to be consistent with the names used by PNG files. + // Unclear from base media type if it has an alpha layer, + // so just assume that it does since it "potentially" could. + switch ( $header['base_type'] ) { + case 0: + $metadata['colorType'] = 'truecolour-alpha'; + break; + case 1: + $metadata['colorType'] = 'greyscale-alpha'; + break; + case 2: + $metadata['colorType'] = 'index-coloured'; + break; + default: + $metadata['colorType'] = 'unknown'; - assert( '7 == count($metadata); # return array must contains 7 elements just like getimagesize() return' ); + } + } else { + // Marker to prevent repeated attempted extraction + $metadata['error'] = true; + } + return serialize( $metadata ); + } - return $metadata; + /** + * Should we refresh the metadata + * + * @param File $file The file object for the file in question + * @param string $metadata Serialized metadata + * @return bool One of the self::METADATA_(BAD|GOOD|COMPATIBLE) constants + */ + public function isMetadataValid( $file, $metadata ) { + if ( !$metadata ) { + // Old metadata when we just put an empty string in there + return self::METADATA_BAD; + } else { + return self::METADATA_GOOD; + } } /** * Must use "im" for XCF * + * @param string $dstPath + * @param bool $checkDstPath * @return string */ - protected static function getScalerType( $dstPath, $checkDstPath = true ) { + protected function getScalerType( $dstPath, $checkDstPath = true ) { return "im"; } + + /** + * Can we render this file? + * + * Image magick doesn't support indexed xcf files as of current + * writing (as of 6.8.9-3) + * @param File $file + * @return bool + */ + public function canRender( $file ) { + wfSuppressWarnings(); + $xcfMeta = unserialize( $file->getMetadata() ); + wfRestoreWarnings(); + if ( isset( $xcfMeta['colorType'] ) && $xcfMeta['colorType'] === 'index-coloured' ) { + return false; + } + return parent::canRender( $file ); + } } diff --git a/includes/media/XMP.php b/includes/media/XMP.php index 7eb3d19e..cdbd5ab2 100644 --- a/includes/media/XMP.php +++ b/includes/media/XMP.php @@ -23,7 +23,7 @@ /** * Class for reading xmp data containing properties relevant to - * images, and spitting out an array that FormatExif accepts. + * images, and spitting out an array that FormatMetadata accepts. * * Note, this is not meant to recognize every possible thing you can * encode in XMP. It should recognize all the properties we want. @@ -34,12 +34,12 @@ * * The public methods one would call in this class are * - parse( $content ) - * Reads in xmp content. - * Can potentially be called multiple times with partial data each time. + * Reads in xmp content. + * Can potentially be called multiple times with partial data each time. * - parseExtended( $content ) - * Reads XMPExtended blocks (jpeg files only). + * Reads XMPExtended blocks (jpeg files only). * - getResults - * Outputs a results array. + * Outputs a results array. * * Note XMP kind of looks like rdf. They are not the same thing - XMP is * encoded as a specific subset of rdf. This class can read XMP. It cannot @@ -47,20 +47,38 @@ * */ class XMPReader { + /** @var array XMP item configuration array */ + protected $items; + + /** @var array Array to hold the current element (and previous element, and so on) */ + private $curItem = array(); + + /** @var bool|string The structure name when processing nested structures. */ + private $ancestorStruct = false; + + /** @var bool|string Temporary holder for character data that appears in xmp doc. */ + private $charContent = false; + + /** @var array Stores the state the xmpreader is in (see MODE_FOO constants) */ + private $mode = array(); + + /** @var array Array to hold results */ + private $results = array(); + + /** @var bool If we're doing a seq or bag. */ + private $processingArray = false; - private $curItem = array(); // array to hold the current element (and previous element, and so on) - private $ancestorStruct = false; // the structure name when processing nested structures. - private $charContent = false; // temporary holder for character data that appears in xmp doc. - private $mode = array(); // stores the state the xmpreader is in (see MODE_FOO constants) - private $results = array(); // array to hold results - private $processingArray = false; // if we're doing a seq or bag. - private $itemLang = false; // used for lang alts only + /** @var bool|string Used for lang alts only */ + private $itemLang = false; + /** @var resource A resource handle for the XML parser */ private $xmlParser; + + /** @var bool|string Character set like 'UTF-8' */ private $charset = false; - private $extendedXMPOffset = 0; - protected $items; + /** @var int */ + private $extendedXMPOffset = 0; /** * These are various mode constants. @@ -105,8 +123,8 @@ class XMPReader { $this->items = XMPInfo::getItems(); $this->resetXMLParser(); - } + /** * Main use is if a single item has multiple xmp documents describing it. * For example in jpeg's with extendedXMP @@ -141,8 +159,8 @@ class XMPReader { /** Get the result array. Do some post-processing before returning * the array, and transform any metadata that is special-cased. * - * @return Array array of results as an array of arrays suitable for - * FormatMetadata::getFormattedData(). + * @return array Array of results as an array of arrays suitable for + * FormatMetadata::getFormattedData(). */ public function getResults() { // xmp-special is for metadata that affects how stuff @@ -155,7 +173,7 @@ class XMPReader { $data = $this->results; - wfRunHooks( 'XMPGetResults', Array( &$data ) ); + wfRunHooks( 'XMPGetResults', array( &$data ) ); if ( isset( $data['xmp-special']['AuthorsPosition'] ) && is_string( $data['xmp-special']['AuthorsPosition'] ) @@ -237,10 +255,10 @@ class XMPReader { * debug log, blanks result array and returns false. * * @param string $content XMP data - * @param $allOfIt Boolean: If this is all the data (true) or if its split up (false). Default true - * @param $reset Boolean: does xml parser need to be reset. Default false + * @param bool $allOfIt If this is all the data (true) or if its split up (false). Default true + * @param bool $reset Does xml parser need to be reset. Default false * @throws MWException - * @return Boolean success. + * @return bool Success. */ public function parse( $content, $allOfIt = true, $reset = false ) { if ( $reset ) { @@ -301,8 +319,10 @@ class XMPReader { } catch ( MWException $e ) { wfDebugLog( 'XMP', 'XMP parse error: ' . $e ); $this->results = array(); + return false; } + return true; } @@ -311,36 +331,43 @@ class XMPReader { * @todo In serious need of testing * @see http://www.adobe.ge/devnet/xmp/pdfs/XMPSpecificationPart3.pdf XMP spec part 3 page 20 * @param string $content XMPExtended block minus the namespace signature - * @return Boolean If it succeeded. + * @return bool If it succeeded. */ public function parseExtended( $content ) { // @todo FIXME: This is untested. Hard to find example files // or programs that make such files.. $guid = substr( $content, 0, 32 ); if ( !isset( $this->results['xmp-special']['HasExtendedXMP'] ) - || $this->results['xmp-special']['HasExtendedXMP'] !== $guid ) { - wfDebugLog( 'XMP', __METHOD__ . " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" ); + || $this->results['xmp-special']['HasExtendedXMP'] !== $guid + ) { + wfDebugLog( 'XMP', __METHOD__ . + " Ignoring XMPExtended block due to wrong guid (guid= '$guid')" ); + return false; } $len = unpack( 'Nlength/Noffset', substr( $content, 32, 8 ) ); if ( !$len || $len['length'] < 4 || $len['offset'] < 0 || $len['offset'] > $len['length'] ) { wfDebugLog( 'XMP', __METHOD__ . 'Error reading extended XMP block, invalid length or offset.' ); + return false; } - // we're not very robust here. we should accept it in the wrong order. To quote - // the xmp standard: - // "A JPEG writer should write the ExtendedXMP marker segments in order, immediately following the - // StandardXMP. However, the JPEG standard does not require preservation of marker segment order. A - // robust JPEG reader should tolerate the marker segments in any order." + // we're not very robust here. we should accept it in the wrong order. + // To quote the XMP standard: + // "A JPEG writer should write the ExtendedXMP marker segments in order, + // immediately following the StandardXMP. However, the JPEG standard + // does not require preservation of marker segment order. A robust JPEG + // reader should tolerate the marker segments in any order." // - // otoh the probability that an image will have more than 128k of metadata is rather low... - // so the probability that it will have > 128k, and be in the wrong order is very low... + // otoh the probability that an image will have more than 128k of + // metadata is rather low... so the probability that it will have + // > 128k, and be in the wrong order is very low... if ( $len['offset'] !== $this->extendedXMPOffset ) { wfDebugLog( 'XMP', __METHOD__ . 'Ignoring XMPExtended block due to wrong order. (Offset was ' . $len['offset'] . ' but expected ' . $this->extendedXMPOffset . ')' ); + return false; } @@ -361,6 +388,7 @@ class XMPReader { } wfDebugLog( 'XMP', __METHOD__ . 'Parsing a XMPExtended block' ); + return $this->parse( $actualContent, $atEnd ); } @@ -376,9 +404,9 @@ class XMPReader { * <exif:DigitalZoomRatio>0/10</exif:DigitalZoomRatio> * and are processing the 0/10 bit. * - * @param $parser XMLParser reference to the xml parser + * @param XMLParser $parser XMLParser reference to the xml parser * @param string $data Character data - * @throws MWException on invalid data + * @throws MWException On invalid data */ function char( $parser, $data ) { @@ -407,7 +435,6 @@ class XMPReader { } else { $this->charContent .= $data; } - } /** When we hit a closing element in MODE_IGNORE @@ -436,7 +463,7 @@ class XMPReader { * Or it could be if we hit the end element of a property * of a compound data structure (like a member of an array). * - * @param string $elm namespace, space, and tag name. + * @param string $elm Namespace, space, and tag name. */ private function endElementModeSimple( $elm ) { if ( $this->charContent !== false ) { @@ -453,7 +480,6 @@ class XMPReader { } array_shift( $this->curItem ); array_shift( $this->mode ); - } /** @@ -471,7 +497,7 @@ class XMPReader { * * This method is called when we hit the "</exif:ISOSpeedRatings>" tag. * - * @param string $elm namespace . space . tag name. + * @param string $elm Namespace . space . tag name. * @throws MWException */ private function endElementNested( $elm ) { @@ -482,7 +508,8 @@ class XMPReader { && !( $elm === self::NS_RDF . ' Description' && $this->mode[0] === self::MODE_STRUCT ) ) { - throw new MWException( "nesting mismatch. got a </$elm> but expected a </" . $this->curItem[0] . '>' ); + throw new MWException( "nesting mismatch. got a </$elm> but expected a </" . + $this->curItem[0] . '>' ); } // Validate structures. @@ -499,7 +526,6 @@ class XMPReader { if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) { // This can happen if all the members of the struct failed validation. wfDebugLog( 'XMP', __METHOD__ . " <$ns:$tag> has no valid members." ); - } elseif ( is_callable( $validate ) ) { $val =& $this->results['xmp-' . $info['map_group']][$finalName]; call_user_func_array( $validate, array( $info, &$val, false ) ); @@ -538,7 +564,7 @@ class XMPReader { * (For comparison, we call endElementModeSimple when we * hit the "</rdf:li>") * - * @param string $elm namespace . ' ' . element name + * @param string $elm Namespace . ' ' . element name * @throws MWException */ private function endElementModeLi( $elm ) { @@ -552,6 +578,7 @@ class XMPReader { if ( !isset( $this->results['xmp-' . $info['map_group']][$finalName] ) ) { wfDebugLog( 'XMP', __METHOD__ . " Empty compund element $finalName." ); + return; } @@ -564,7 +591,6 @@ class XMPReader { if ( $info['mode'] === self::MODE_LANG ) { $this->results['xmp-' . $info['map_group']][$finalName]['_type'] = 'lang'; } - } else { throw new MWException( __METHOD__ . " expected </rdf:seq> or </rdf:bag> but instead got $elm." ); } @@ -578,13 +604,14 @@ class XMPReader { * Qualifiers aren't all that common, and we don't do anything * with them. * - * @param string $elm namespace and element + * @param string $elm Namespace and element */ private function endElementModeQDesc( $elm ) { if ( $elm === self::NS_RDF . ' value' ) { list( $ns, $tag ) = explode( ' ', $this->curItem[0], 2 ); $this->saveValue( $ns, $tag, $this->charContent ); + return; } else { array_shift( $this->mode ); @@ -601,15 +628,15 @@ class XMPReader { * Ignores the outer wrapping elements that are optional in * xmp and have no meaning. * - * @param $parser XMLParser - * @param string $elm namespace . ' ' . element name + * @param XMLParser $parser + * @param string $elm Namespace . ' ' . element name * @throws MWException */ function endElement( $parser, $elm ) { if ( $elm === ( self::NS_RDF . ' RDF' ) || $elm === 'adobe:ns:meta/ xmpmeta' - || $elm === 'adobe:ns:meta/ xapmeta' ) - { + || $elm === 'adobe:ns:meta/ xapmeta' + ) { // ignore these. return; } @@ -626,6 +653,7 @@ class XMPReader { // that forgets the namespace on some things. // (Luckily they are unimportant things). wfDebugLog( 'XMP', __METHOD__ . " Encountered </$elm> which has no namespace. Skipping." ); + return; } @@ -684,7 +712,7 @@ class XMPReader { * in which case we add it to the item stack, so we can ignore things * that are nested, correctly. * - * @param string $elm namespace . ' ' . tag name + * @param string $elm Namespace . ' ' . tag name */ private function startElementModeIgnore( $elm ) { if ( $elm === $this->curItem[0] ) { @@ -697,8 +725,8 @@ class XMPReader { * Start element in MODE_BAG (unordered array) * this should always be <rdf:Bag> * - * @param string $elm namespace . ' ' . tag - * @throws MWException if we have an element that's not <rdf:Bag> + * @param string $elm Namespace . ' ' . tag + * @throws MWException If we have an element that's not <rdf:Bag> */ private function startElementModeBag( $elm ) { if ( $elm === self::NS_RDF . ' Bag' ) { @@ -706,15 +734,14 @@ class XMPReader { } else { throw new MWException( "Expected <rdf:Bag> but got $elm." ); } - } /** * Start element in MODE_SEQ (ordered array) * this should always be <rdf:Seq> * - * @param string $elm namespace . ' ' . tag - * @throws MWException if we have an element that's not <rdf:Seq> + * @param string $elm Namespace . ' ' . tag + * @throws MWException If we have an element that's not <rdf:Seq> */ private function startElementModeSeq( $elm ) { if ( $elm === self::NS_RDF . ' Seq' ) { @@ -727,7 +754,6 @@ class XMPReader { } else { throw new MWException( "Expected <rdf:Seq> but got $elm." ); } - } /** @@ -741,8 +767,8 @@ class XMPReader { * which are really only used for thumbnails, which * we don't care about. * - * @param string $elm namespace . ' ' . tag - * @throws MWException if we have an element that's not <rdf:Alt> + * @param string $elm Namespace . ' ' . tag + * @throws MWException If we have an element that's not <rdf:Alt> */ private function startElementModeLang( $elm ) { if ( $elm === self::NS_RDF . ' Alt' ) { @@ -750,7 +776,6 @@ class XMPReader { } else { throw new MWException( "Expected <rdf:Seq> but got $elm." ); } - } /** @@ -767,7 +792,7 @@ class XMPReader { * * This method is called when processing the <rdf:Description> element * - * @param string $elm namespace and tag names separated by space. + * @param string $elm Namespace and tag names separated by space. * @param array $attribs Attributes of the element. * @throws MWException */ @@ -784,15 +809,14 @@ class XMPReader { } elseif ( $elm === self::NS_RDF . ' value' ) { // This should not be here. throw new MWException( __METHOD__ . ' Encountered <rdf:value> where it was unexpected.' ); - } else { // something else we don't recognize, like a qualifier maybe. - wfDebugLog( 'XMP', __METHOD__ . " Encountered element <$elm> where only expecting character data as value of " . $this->curItem[0] ); + wfDebugLog( 'XMP', __METHOD__ . + " Encountered element <$elm> where only expecting character data as value of " . + $this->curItem[0] ); array_unshift( $this->mode, self::MODE_IGNORE ); array_unshift( $this->curItem, $elm ); - } - } /** @@ -806,7 +830,7 @@ class XMPReader { * </exif:DigitalZoomRatio> * Called when processing the <rdf:value> or <foo:someQualifier>. * - * @param string $elm namespace and tag name separated by a space. + * @param string $elm Namespace and tag name separated by a space. * */ private function startElementModeQDesc( $elm ) { @@ -827,8 +851,8 @@ class XMPReader { * This is generally where most properties start. * * @param string $ns Namespace - * @param string $tag tag name (without namespace prefix) - * @param array $attribs array of attributes + * @param string $tag Tag name (without namespace prefix) + * @param array $attribs Array of attributes * @throws MWException */ private function startElementModeInitial( $ns, $tag, $attribs ) { @@ -846,6 +870,7 @@ class XMPReader { array_unshift( $this->mode, self::MODE_IGNORE ); array_unshift( $this->curItem, $ns . ' ' . $tag ); + return; } $mode = $this->items[$ns][$tag]['mode']; @@ -865,9 +890,9 @@ class XMPReader { wfDebugLog( 'XMP', __METHOD__ . " Ignoring unrecognized element <$ns:$tag>." ); array_unshift( $this->mode, self::MODE_IGNORE ); array_unshift( $this->curItem, $ns . ' ' . $tag ); + return; } - } // process attributes $this->doAttribs( $attribs ); @@ -887,9 +912,9 @@ class XMPReader { * <exif:Flash rdf:parseType='Resource'> <exif:Fired>True</exif:Fired> * <exif:Mode>1</exif:Mode></exif:Flash> * - * @param string $ns namespace - * @param string $tag tag name (no ns) - * @param array $attribs array of attribs w/ values. + * @param string $ns Namespace + * @param string $tag Tag name (no ns) + * @param array $attribs Array of attribs w/ values. * @throws MWException */ private function startElementModeStruct( $ns, $tag, $attribs ) { @@ -897,8 +922,8 @@ class XMPReader { if ( isset( $this->items[$ns][$tag] ) ) { if ( isset( $this->items[$ns][$this->ancestorStruct]['children'] ) - && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] ) ) - { + && !isset( $this->items[$ns][$this->ancestorStruct]['children'][$tag] ) + ) { // This assumes that we don't have inter-namespace nesting // which we don't in all the properties we're interested in. throw new MWException( " <$tag> appeared nested in <" . $this->ancestorStruct @@ -909,14 +934,15 @@ class XMPReader { if ( $this->charContent !== false ) { // Something weird. // Should not happen in valid XMP. - throw new MWException( "tag <$tag> nested in non-whitespace characters (" . $this->charContent . ")." ); + throw new MWException( "tag <$tag> nested in non-whitespace characters (" . + $this->charContent . ")." ); } } else { array_unshift( $this->mode, self::MODE_IGNORE ); array_unshift( $this->curItem, $elm ); + return; } - } if ( $ns === self::NS_RDF && $tag === 'Description' ) { @@ -935,9 +961,9 @@ class XMPReader { * </rdf:Seq> </exif:ISOSpeedRatings> * This method is called when we hit the <rdf:li> element. * - * @param string $elm namespace . ' ' . tagname + * @param string $elm Namespace . ' ' . tagname * @param array $attribs Attributes. (needed for BAGSTRUCTS) - * @throws MWException if gets a tag other than <rdf:li> + * @throws MWException If gets a tag other than <rdf:li> */ private function startElementModeLi( $elm, $attribs ) { if ( ( $elm ) !== self::NS_RDF . ' li' ) { @@ -965,7 +991,6 @@ class XMPReader { ? $this->items[$curNS][$curTag]['map_name'] : $curTag; $this->doAttribs( $attribs ); - } else { // Normal BAG or SEQ containing simple values. array_unshift( $this->mode, self::MODE_SIMPLE ); @@ -974,7 +999,6 @@ class XMPReader { array_unshift( $this->curItem, $this->curItem[0] ); $this->processingArray = true; } - } /** @@ -987,17 +1011,17 @@ class XMPReader { * * This method is called when we hit the <rdf:li> element. * - * @param string $elm namespace . ' ' . tag - * @param array $attribs array of elements (most importantly xml:lang) - * @throws MWException if gets a tag other than <rdf:li> or if no xml:lang + * @param string $elm Namespace . ' ' . tag + * @param array $attribs Array of elements (most importantly xml:lang) + * @throws MWException If gets a tag other than <rdf:li> or if no xml:lang */ private function startElementModeLiLang( $elm, $attribs ) { if ( $elm !== self::NS_RDF . ' li' ) { throw new MWException( __METHOD__ . " <rdf:li> expected but got $elm." ); } if ( !isset( $attribs[self::NS_XML . ' lang'] ) - || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] ) ) - { + || !preg_match( '/^[-A-Za-z0-9]{2,}$/D', $attribs[self::NS_XML . ' lang'] ) + ) { throw new MWException( __METHOD__ . " <rdf:li> did not contain, or has invalid xml:lang attribute in lang alternative" ); } @@ -1017,17 +1041,17 @@ class XMPReader { * Generally just calls a helper based on what MODE we're in. * Also does some initial set up for the wrapper element * - * @param $parser XMLParser - * @param string $elm namespace "<space>" element - * @param array $attribs attribute name => value + * @param XMLParser $parser + * @param string $elm Namespace "<space>" element + * @param array $attribs Attribute name => value * @throws MWException */ function startElement( $parser, $elm, $attribs ) { if ( $elm === self::NS_RDF . ' RDF' || $elm === 'adobe:ns:meta/ xmpmeta' - || $elm === 'adobe:ns:meta/ xapmeta' ) - { + || $elm === 'adobe:ns:meta/ xapmeta' + ) { /* ignore. */ return; } elseif ( $elm === self::NS_RDF . ' Description' ) { @@ -1049,6 +1073,7 @@ class XMPReader { if ( strpos( $elm, ' ' ) === false ) { // This probably shouldn't happen. wfDebugLog( 'XMP', __METHOD__ . " Encountered <$elm> which has no namespace. Skipping." ); + return; } @@ -1104,23 +1129,24 @@ class XMPReader { * Often the initial "<rdf:Description>" tag just has all the simple * properties as attributes. * + * @codingStandardsIgnoreStart Long line that cannot be broken * @par Example: * @code * <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/" exif:DigitalZoomRatio="0/10"> * @endcode + * @codingStandardsIgnoreEnd * - * @param array $attribs attribute=>value array. + * @param array $attribs Array attribute=>value * @throws MWException */ private function doAttribs( $attribs ) { - // first check for rdf:parseType attribute, as that can change // how the attributes are interperted. if ( isset( $attribs[self::NS_RDF . ' parseType'] ) && $attribs[self::NS_RDF . ' parseType'] === 'Resource' - && $this->mode[0] === self::MODE_SIMPLE ) - { + && $this->mode[0] === self::MODE_SIMPLE + ) { // this is equivalent to having an inner rdf:Description $this->mode[0] = self::MODE_QDESC; } @@ -1158,9 +1184,9 @@ class XMPReader { * $this->processingArray to determine what name to * save the value under. (in addition to $tag). * - * @param string $ns namespace of tag this is for - * @param string $tag tag name - * @param string $val value to save + * @param string $ns Namespace of tag this is for + * @param string $tag Tag name + * @param string $val Value to save */ private function saveValue( $ns, $tag, $val ) { @@ -1177,6 +1203,7 @@ class XMPReader { // is to be consistent between here and validating structures. if ( is_null( $val ) ) { wfDebugLog( 'XMP', __METHOD__ . " <$ns:$tag> failed validation." ); + return; } } else { diff --git a/includes/media/XMPInfo.php b/includes/media/XMPInfo.php index f0b2cb5e..7e47ec14 100644 --- a/includes/media/XMPInfo.php +++ b/includes/media/XMPInfo.php @@ -27,17 +27,17 @@ * extract. */ class XMPInfo { - - /** get the items array - * @return Array XMP item configuration array. + /** Get the items array + * @return array XMP item configuration array. */ public static function getItems() { if ( !self::$ranHooks ) { // This is for if someone makes a custom metadata extension. // For example, a medical wiki might want to decode DICOM xmp properties. - wfRunHooks( 'XMPGetInfo', Array( &self::$items ) ); + wfRunHooks( 'XMPGetInfo', array( &self::$items ) ); self::$ranHooks = true; // Only want to do this once. } + return self::$items; } @@ -53,382 +53,388 @@ class XMPInfo { * each containing an array of tags * each tag is an array of information about the * tag, including: - * * map_group - what group (used for precedence during conflicts) - * * mode - What type of item (self::MODE_SIMPLE usually, see above for all values) - * * validate - method to validate input. Could also post-process the input. A string value is assumed to be a static method of XMPValidate. Can also take a array( 'className', 'methodName' ). - * * choices - array of potential values (format of 'value' => true ). Only used with validateClosed - * * rangeLow and rangeHigh - alternative to choices for numeric ranges. Again for validateClosed only. - * * children - for MODE_STRUCT items, allowed children. - * * structPart - Indicates that this element can only appear as a member of a structure. + * * map_group - What group (used for precedence during conflicts). + * * mode - What type of item (self::MODE_SIMPLE usually, see above for + * all values). + * * validate - Method to validate input. Could also post-process the + * input. A string value is assumed to be a static method of + * XMPValidate. Can also take a array( 'className', 'methodName' ). + * * choices - Array of potential values (format of 'value' => true ). + * Only used with validateClosed. + * * rangeLow and rangeHigh - Alternative to choices for numeric ranges. + * Again for validateClosed only. + * * children - For MODE_STRUCT items, allowed children. + * * structPart - Indicates that this element can only appear as a member + * of a structure. * - * currently this just has a bunch of exif values as this class is only half-done + * Currently this just has a bunch of EXIF values as this class is only half-done. */ static private $items = array( 'http://ns.adobe.com/exif/1.0/' => array( 'ApertureValue' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'BrightnessValue' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'CompressedBitsPerPixel' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'DigitalZoomRatio' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'ExposureBiasValue' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'ExposureIndex' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'ExposureTime' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'FlashEnergy' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational', ), 'FNumber' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'FocalLength' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'FocalPlaneXResolution' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'FocalPlaneYResolution' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSAltitude' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational', ), 'GPSDestBearing' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSDestDistance' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSDOP' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSImgDirection' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSSpeed' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'GPSTrack' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), - 'MaxApertureValue' => array( + 'MaxApertureValue' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), 'ShutterSpeedValue' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), - 'SubjectDistance' => array( + 'SubjectDistance' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational' + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational' ), /* Flash */ - 'Flash' => array( - 'mode' => XMPReader::MODE_STRUCT, - 'children' => array( - 'Fired' => true, - 'Function' => true, - 'Mode' => true, + 'Flash' => array( + 'mode' => XMPReader::MODE_STRUCT, + 'children' => array( + 'Fired' => true, + 'Function' => true, + 'Mode' => true, 'RedEyeMode' => true, - 'Return' => true, + 'Return' => true, ), - 'validate' => 'validateFlash', + 'validate' => 'validateFlash', 'map_group' => 'exif', ), - 'Fired' => array( + 'Fired' => array( 'map_group' => 'exif', - 'validate' => 'validateBoolean', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'validate' => 'validateBoolean', + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), - 'Function' => array( + 'Function' => array( 'map_group' => 'exif', - 'validate' => 'validateBoolean', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'validate' => 'validateBoolean', + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), - 'Mode' => array( + 'Mode' => array( 'map_group' => 'exif', - 'validate' => 'validateClosed', - 'mode' => XMPReader::MODE_SIMPLE, - 'choices' => array( '0' => true, '1' => true, - '2' => true, '3' => true ), - 'structPart'=> true, + 'validate' => 'validateClosed', + 'mode' => XMPReader::MODE_SIMPLE, + 'choices' => array( '0' => true, '1' => true, + '2' => true, '3' => true ), + 'structPart' => true, ), - 'Return' => array( + 'Return' => array( 'map_group' => 'exif', - 'validate' => 'validateClosed', - 'mode' => XMPReader::MODE_SIMPLE, - 'choices' => array( '0' => true, - '2' => true, '3' => true ), - 'structPart'=> true, + 'validate' => 'validateClosed', + 'mode' => XMPReader::MODE_SIMPLE, + 'choices' => array( '0' => true, + '2' => true, '3' => true ), + 'structPart' => true, ), - 'RedEyeMode' => array( + 'RedEyeMode' => array( 'map_group' => 'exif', - 'validate' => 'validateBoolean', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'validate' => 'validateBoolean', + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), /* End Flash */ - 'ISOSpeedRatings' => array( + 'ISOSpeedRatings' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateInteger' + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateInteger' ), /* end rational things */ - 'ColorSpace' => array( + 'ColorSpace' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '65535' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true, '65535' => true ), ), - 'ComponentsConfiguration' => array( + 'ComponentsConfiguration' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '2' => true, '3' => true, '4' => true, - '5' => true, '6' => true ) + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true, '2' => true, '3' => true, '4' => true, + '5' => true, '6' => true ) ), - 'Contrast' => array( + 'Contrast' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '0' => true, '1' => true, '2' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '0' => true, '1' => true, '2' => true ) ), - 'CustomRendered' => array( + 'CustomRendered' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '0' => true, '1' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '0' => true, '1' => true ) ), - 'DateTimeOriginal' => array( + 'DateTimeOriginal' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateDate', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateDate', ), 'DateTimeDigitized' => array( /* xmp:CreateDate */ 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateDate', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateDate', ), /* todo: there might be interesting information in * exif:DeviceSettingDescription, but need to find an * example */ - 'ExifVersion' => array( + 'ExifVersion' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'ExposureMode' => array( + 'ExposureMode' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 2, ), - 'ExposureProgram' => array( + 'ExposureProgram' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 8, ), - 'FileSource' => array( + 'FileSource' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '3' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '3' => true ) ), - 'FlashpixVersion' => array( + 'FlashpixVersion' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'FocalLengthIn35mmFilm' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), 'FocalPlaneResolutionUnit' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '2' => true, '3' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '2' => true, '3' => true ), ), - 'GainControl' => array( + 'GainControl' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 4, ), /* this value is post-processed out later */ - 'GPSAltitudeRef' => array( + 'GPSAltitudeRef' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '0' => true, '1' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '0' => true, '1' => true ), ), 'GPSAreaInformation' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'GPSDestBearingRef' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'T' => true, 'M' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'T' => true, 'M' => true ), ), 'GPSDestDistanceRef' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'K' => true, 'M' => true, - 'N' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'K' => true, 'M' => true, + 'N' => true ), ), - 'GPSDestLatitude' => array( + 'GPSDestLatitude' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateGPS', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateGPS', ), - 'GPSDestLongitude' => array( + 'GPSDestLongitude' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateGPS', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateGPS', ), - 'GPSDifferential' => array( + 'GPSDifferential' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '0' => true, '1' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '0' => true, '1' => true ), ), 'GPSImgDirectionRef' => array( - 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'T' => true, 'M' => true ), + 'map_group' => 'exif', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'T' => true, 'M' => true ), ), - 'GPSLatitude' => array( + 'GPSLatitude' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateGPS', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateGPS', ), - 'GPSLongitude' => array( + 'GPSLongitude' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateGPS', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateGPS', ), - 'GPSMapDatum' => array( + 'GPSMapDatum' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'GPSMeasureMode' => array( + 'GPSMeasureMode' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '2' => true, '3' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '2' => true, '3' => true ) ), 'GPSProcessingMethod' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'GPSSatellites' => array( + 'GPSSatellites' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'GPSSpeedRef' => array( + 'GPSSpeedRef' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'K' => true, 'M' => true, - 'N' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'K' => true, 'M' => true, + 'N' => true ), ), - 'GPSStatus' => array( + 'GPSStatus' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'A' => true, 'V' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'A' => true, 'V' => true ) ), - 'GPSTimeStamp' => array( + 'GPSTimeStamp' => array( 'map_group' => 'exif', // Note: in exif, GPSDateStamp does not include // the time, where here it does. - 'map_name' => 'GPSDateStamp', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateDate', + 'map_name' => 'GPSDateStamp', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateDate', ), - 'GPSTrackRef' => array( + 'GPSTrackRef' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( 'T' => true, 'M' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( 'T' => true, 'M' => true ) ), - 'GPSVersionID' => array( + 'GPSVersionID' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'ImageUniqueID' => array( + 'ImageUniqueID' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'LightSource' => array( + 'LightSource' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', /* can't use a range, as it skips... */ - 'choices' => array( '0' => true, '1' => true, + 'choices' => array( '0' => true, '1' => true, '2' => true, '3' => true, '4' => true, '9' => true, '10' => true, '11' => true, '12' => true, '13' => true, @@ -440,217 +446,217 @@ class XMPInfo { '255' => true, ), ), - 'MeteringMode' => array( + 'MeteringMode' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 6, - 'choices' => array( '255' => true ), + 'choices' => array( '255' => true ), ), /* Pixel(X|Y)Dimension are rather useless, but for * completeness since we do it with exif. */ - 'PixelXDimension' => array( + 'PixelXDimension' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), - 'PixelYDimension' => array( + 'PixelYDimension' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), - 'Saturation' => array( + 'Saturation' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 2, ), - 'SceneCaptureType' => array( + 'SceneCaptureType' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 3, ), - 'SceneType' => array( + 'SceneType' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true ), ), // Note, 6 is not valid SensingMethod. - 'SensingMethod' => array( + 'SensingMethod' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 1, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 1, 'rangeHigh' => 5, - 'choices' => array( '7' => true, 8 => true ), + 'choices' => array( '7' => true, 8 => true ), ), - 'Sharpness' => array( + 'Sharpness' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 2, ), 'SpectralSensitivity' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), // This tag should perhaps be displayed to user better. - 'SubjectArea' => array( + 'SubjectArea' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateInteger', ), 'SubjectDistanceRange' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'rangeLow' => 0, + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'rangeLow' => 0, 'rangeHigh' => 3, ), - 'SubjectLocation' => array( + 'SubjectLocation' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateInteger', ), - 'UserComment' => array( + 'UserComment' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_LANG, + 'mode' => XMPReader::MODE_LANG, ), - 'WhiteBalance' => array( + 'WhiteBalance' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '0' => true, '1' => true ) + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '0' => true, '1' => true ) ), ), 'http://ns.adobe.com/tiff/1.0/' => array( - 'Artist' => array( + 'Artist' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'BitsPerSample' => array( + 'BitsPerSample' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateInteger', ), - 'Compression' => array( + 'Compression' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '6' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true, '6' => true ), ), /* this prop should not be used in XMP. dc:rights is the correct prop */ - 'Copyright' => array( + 'Copyright' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_LANG, + 'mode' => XMPReader::MODE_LANG, ), - 'DateTime' => array( /* proper prop is xmp:ModifyDate */ + 'DateTime' => array( /* proper prop is xmp:ModifyDate */ 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateDate', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateDate', ), - 'ImageDescription' => array( /* proper one is dc:description */ + 'ImageDescription' => array( /* proper one is dc:description */ 'map_group' => 'exif', - 'mode' => XMPReader::MODE_LANG, + 'mode' => XMPReader::MODE_LANG, ), - 'ImageLength' => array( + 'ImageLength' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), - 'ImageWidth' => array( + 'ImageWidth' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), - 'Make' => array( + 'Make' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'Model' => array( + 'Model' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), /**** Do not extract this property * It interferes with auto exif rotation. * 'Orientation' => array( - * 'map_group' => 'exif', - * 'mode' => XMPReader::MODE_SIMPLE, - * 'validate' => 'validateClosed', - * 'choices' => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true, - * '6' => true, '7' => true, '8' => true ), + * 'map_group' => 'exif', + * 'mode' => XMPReader::MODE_SIMPLE, + * 'validate' => 'validateClosed', + * 'choices' => array( '1' => true, '2' => true, '3' => true, '4' => true, 5 => true, + * '6' => true, '7' => true, '8' => true ), *), ******/ 'PhotometricInterpretation' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '2' => true, '6' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '2' => true, '6' => true ), ), 'PlanerConfiguration' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '2' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true, '2' => true ), ), 'PrimaryChromaticities' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateRational', ), 'ReferenceBlackWhite' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateRational', ), - 'ResolutionUnit' => array( + 'ResolutionUnit' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '2' => true, '3' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '2' => true, '3' => true ), ), - 'SamplesPerPixel' => array( + 'SamplesPerPixel' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', ), - 'Software' => array( /* see xmp:CreatorTool */ + 'Software' => array( /* see xmp:CreatorTool */ 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), /* ignore TransferFunction */ - 'WhitePoint' => array( + 'WhitePoint' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateRational', ), - 'XResolution' => array( + 'XResolution' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational', ), - 'YResolution' => array( + 'YResolution' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRational', ), 'YCbCrCoefficients' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateRational', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateRational', ), - 'YCbCrPositioning' => array( + 'YCbCrPositioning' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateClosed', - 'choices' => array( '1' => true, '2' => true ), + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateClosed', + 'choices' => array( '1' => true, '2' => true ), ), /******** * Disable extracting this property (bug 31944) @@ -663,170 +669,170 @@ class XMPInfo { * just disable this prop for now, until such * XMPReader is more graceful (bug 32172) * 'YCbCrSubSampling' => array( - * 'map_group' => 'exif', - * 'mode' => XMPReader::MODE_SEQ, - * 'validate' => 'validateClosed', - * 'choices' => array( '1' => true, '2' => true ), + * 'map_group' => 'exif', + * 'mode' => XMPReader::MODE_SEQ, + * 'validate' => 'validateClosed', + * 'choices' => array( '1' => true, '2' => true ), * ), */ ), 'http://ns.adobe.com/exif/1.0/aux/' => array( - 'Lens' => array( + 'Lens' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'SerialNumber' => array( + 'SerialNumber' => array( 'map_group' => 'exif', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), - 'OwnerName' => array( + 'OwnerName' => array( 'map_group' => 'exif', - 'map_name' => 'CameraOwnerName', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'CameraOwnerName', + 'mode' => XMPReader::MODE_SIMPLE, ), ), 'http://purl.org/dc/elements/1.1/' => array( - 'title' => array( + 'title' => array( 'map_group' => 'general', - 'map_name' => 'ObjectName', - 'mode' => XMPReader::MODE_LANG + 'map_name' => 'ObjectName', + 'mode' => XMPReader::MODE_LANG ), - 'description' => array( + 'description' => array( 'map_group' => 'general', - 'map_name' => 'ImageDescription', - 'mode' => XMPReader::MODE_LANG + 'map_name' => 'ImageDescription', + 'mode' => XMPReader::MODE_LANG ), - 'contributor' => array( + 'contributor' => array( 'map_group' => 'general', - 'map_name' => 'dc-contributor', - 'mode' => XMPReader::MODE_BAG + 'map_name' => 'dc-contributor', + 'mode' => XMPReader::MODE_BAG ), - 'coverage' => array( + 'coverage' => array( 'map_group' => 'general', - 'map_name' => 'dc-coverage', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'dc-coverage', + 'mode' => XMPReader::MODE_SIMPLE, ), - 'creator' => array( + 'creator' => array( 'map_group' => 'general', - 'map_name' => 'Artist', //map with exif Artist, iptc byline (2:80) - 'mode' => XMPReader::MODE_SEQ, + 'map_name' => 'Artist', //map with exif Artist, iptc byline (2:80) + 'mode' => XMPReader::MODE_SEQ, ), - 'date' => array( + 'date' => array( 'map_group' => 'general', // Note, not mapped with other date properties, as this type of date is // non-specific: "A point or period of time associated with an event in // the lifecycle of the resource" - 'map_name' => 'dc-date', - 'mode' => XMPReader::MODE_SEQ, - 'validate' => 'validateDate', + 'map_name' => 'dc-date', + 'mode' => XMPReader::MODE_SEQ, + 'validate' => 'validateDate', ), - /* Do not extract dc:format, as we've got better ways to determine mimetype */ - 'identifier' => array( + /* Do not extract dc:format, as we've got better ways to determine MIME type */ + 'identifier' => array( 'map_group' => 'deprecated', - 'map_name' => 'Identifier', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'Identifier', + 'mode' => XMPReader::MODE_SIMPLE, ), - 'language' => array( + 'language' => array( 'map_group' => 'general', - 'map_name' => 'LanguageCode', /* mapped with iptc 2:135 */ - 'mode' => XMPReader::MODE_BAG, - 'validate' => 'validateLangCode', + 'map_name' => 'LanguageCode', /* mapped with iptc 2:135 */ + 'mode' => XMPReader::MODE_BAG, + 'validate' => 'validateLangCode', ), - 'publisher' => array( + 'publisher' => array( 'map_group' => 'general', - 'map_name' => 'dc-publisher', - 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'dc-publisher', + 'mode' => XMPReader::MODE_BAG, ), // for related images/resources - 'relation' => array( + 'relation' => array( 'map_group' => 'general', - 'map_name' => 'dc-relation', - 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'dc-relation', + 'mode' => XMPReader::MODE_BAG, ), - 'rights' => array( + 'rights' => array( 'map_group' => 'general', - 'map_name' => 'Copyright', - 'mode' => XMPReader::MODE_LANG, + 'map_name' => 'Copyright', + 'mode' => XMPReader::MODE_LANG, ), // Note: source is not mapped with iptc source, since iptc // source describes the source of the image in terms of a person // who provided the image, where this is to describe an image that the // current one is based on. - 'source' => array( + 'source' => array( 'map_group' => 'general', - 'map_name' => 'dc-source', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'dc-source', + 'mode' => XMPReader::MODE_SIMPLE, ), - 'subject' => array( + 'subject' => array( 'map_group' => 'general', - 'map_name' => 'Keywords', /* maps to iptc 2:25 */ - 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'Keywords', /* maps to iptc 2:25 */ + 'mode' => XMPReader::MODE_BAG, ), - 'type' => array( + 'type' => array( 'map_group' => 'general', - 'map_name' => 'dc-type', - 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'dc-type', + 'mode' => XMPReader::MODE_BAG, ), ), 'http://ns.adobe.com/xap/1.0/' => array( 'CreateDate' => array( 'map_group' => 'general', 'map_name' => 'DateTimeDigitized', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, 'validate' => 'validateDate', ), 'CreatorTool' => array( 'map_group' => 'general', - 'map_name' => 'Software', - 'mode' => XMPReader::MODE_SIMPLE + 'map_name' => 'Software', + 'mode' => XMPReader::MODE_SIMPLE ), 'Identifier' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, + 'mode' => XMPReader::MODE_BAG, ), 'Label' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'ModifyDate' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'DateTime', - 'validate' => 'validateDate', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'DateTime', + 'validate' => 'validateDate', ), 'MetadataDate' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, // map_name to be consistent with other date names. - 'map_name' => 'DateTimeMetadata', - 'validate' => 'validateDate', + 'map_name' => 'DateTimeMetadata', + 'validate' => 'validateDate', ), 'Nickname' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'Rating' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateRating', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateRating', ), ), 'http://ns.adobe.com/xap/1.0/rights/' => array( 'Certificate' => array( 'map_group' => 'general', - 'map_name' => 'RightsCertificate', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'RightsCertificate', + 'mode' => XMPReader::MODE_SIMPLE, ), 'Marked' => array( 'map_group' => 'general', - 'map_name' => 'Copyrighted', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateBoolean', + 'map_name' => 'Copyrighted', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateBoolean', ), 'Owner' => array( 'map_group' => 'general', - 'map_name' => 'CopyrightOwner', - 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'CopyrightOwner', + 'mode' => XMPReader::MODE_BAG, ), // this seems similar to dc:rights. 'UsageTerms' => array( @@ -844,7 +850,7 @@ class XMPInfo { // as well do this too. 'OriginalDocumentID' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), // It might also be useful to do xmpMM:LastURL // and xmpMM:DerivedFrom as you can potentially, @@ -855,31 +861,31 @@ class XMPInfo { ), 'http://creativecommons.org/ns#' => array( 'license' => array( - 'map_name' => 'LicenseUrl', + 'map_name' => 'LicenseUrl', 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'morePermissions' => array( - 'map_name' => 'MorePermissionsUrl', + 'map_name' => 'MorePermissionsUrl', 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'attributionURL' => array( 'map_group' => 'general', - 'map_name' => 'AttributionUrl', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'AttributionUrl', + 'mode' => XMPReader::MODE_SIMPLE, ), 'attributionName' => array( 'map_group' => 'general', - 'map_name' => 'PreferredAttributionName', - 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'PreferredAttributionName', + 'mode' => XMPReader::MODE_SIMPLE, ), ), //Note, this property affects how jpeg metadata is extracted. 'http://ns.adobe.com/xmp/note/' => array( 'HasExtendedXMP' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), ), /* Note, in iptc schemas, the legacy properties are denoted @@ -890,41 +896,41 @@ class XMPInfo { 'http://ns.adobe.com/photoshop/1.0/' => array( 'City' => array( 'map_group' => 'deprecated', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'CityDest', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'CityDest', ), 'Country' => array( 'map_group' => 'deprecated', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'CountryDest', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'CountryDest', ), 'State' => array( 'map_group' => 'deprecated', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'ProvinceOrStateDest', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'ProvinceOrStateDest', ), 'DateCreated' => array( 'map_group' => 'deprecated', // marking as deprecated as the xmp prop preferred - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'DateTimeOriginal', - 'validate' => 'validateDate', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'DateTimeOriginal', + 'validate' => 'validateDate', // note this prop is an XMP, not IPTC date ), 'CaptionWriter' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'Writer', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'Writer', ), 'Instructions' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'SpecialInstructions', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'SpecialInstructions', ), 'TransmissionReference' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'OriginalTransmissionRef', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'OriginalTransmissionRef', ), 'AuthorsPosition' => array( /* This corresponds with 2:85 @@ -932,46 +938,46 @@ class XMPInfo { * handled weirdly to correspond * with iptc/exif. */ 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE + 'mode' => XMPReader::MODE_SIMPLE ), 'Credit' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'Source' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'Urgency' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'Category' => array( // Note, this prop is deprecated, but in general // group since it doesn't have a replacement. 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'iimCategory', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'iimCategory', ), 'SupplementalCategories' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, - 'map_name' => 'iimSupplementalCategory', + 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'iimSupplementalCategory', ), 'Headline' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE + 'mode' => XMPReader::MODE_SIMPLE ), ), 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' => array( 'CountryCode' => array( 'map_group' => 'deprecated', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'CountryCodeDest', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'CountryCodeDest', ), 'IntellectualGenre' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), // Note, this is a six digit code. // See: http://cv.iptc.org/newscodes/scene/ @@ -979,9 +985,9 @@ class XMPInfo { // we just show the number. 'Scene' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, - 'validate' => 'validateInteger', - 'map_name' => 'SceneCode', + 'mode' => XMPReader::MODE_BAG, + 'validate' => 'validateInteger', + 'map_name' => 'SceneCode', ), /* Note: SubjectCode should be an 8 ascii digits. * it is not really an integer (has leading 0's, @@ -990,14 +996,14 @@ class XMPInfo { */ 'SubjectCode' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, - 'map_name' => 'SubjectNewsCode', - 'validate' => 'validateInteger' + 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'SubjectNewsCode', + 'validate' => 'validateInteger' ), 'Location' => array( 'map_group' => 'deprecated', - 'mode' => XMPReader::MODE_SIMPLE, - 'map_name' => 'SublocationDest', + 'mode' => XMPReader::MODE_SIMPLE, + 'map_name' => 'SublocationDest', ), 'CreatorContactInfo' => array( /* Note this maps to 2:118 in iim @@ -1007,94 +1013,94 @@ class XMPInfo { * is more structured. */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_STRUCT, - 'map_name' => 'Contact', - 'children' => array( + 'mode' => XMPReader::MODE_STRUCT, + 'map_name' => 'Contact', + 'children' => array( 'CiAdrExtadr' => true, - 'CiAdrCity' => true, - 'CiAdrCtry' => true, + 'CiAdrCity' => true, + 'CiAdrCtry' => true, 'CiEmailWork' => true, - 'CiTelWork' => true, - 'CiAdrPcode' => true, + 'CiTelWork' => true, + 'CiAdrPcode' => true, 'CiAdrRegion' => true, - 'CiUrlWork' => true, + 'CiUrlWork' => true, ), ), 'CiAdrExtadr' => array( /* address */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiAdrCity' => array( /* city */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiAdrCtry' => array( /* country */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiEmailWork' => array( /* email (possibly separated by ',') */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiTelWork' => array( /* telephone */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiAdrPcode' => array( /* postal code */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiAdrRegion' => array( /* province/state */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CiUrlWork' => array( /* url. Multiple may be separated by comma. */ 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), /* End contact info struct properties */ ), 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' => array( 'Event' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, + 'mode' => XMPReader::MODE_SIMPLE, ), 'OrganisationInImageName' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, - 'map_name' => 'OrganisationInImage' + 'mode' => XMPReader::MODE_BAG, + 'map_name' => 'OrganisationInImage' ), 'PersonInImage' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_BAG, + 'mode' => XMPReader::MODE_BAG, ), 'MaxAvailHeight' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', - 'map_name' => 'OriginalImageHeight', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', + 'map_name' => 'OriginalImageHeight', ), 'MaxAvailWidth' => array( 'map_group' => 'general', - 'mode' => XMPReader::MODE_SIMPLE, - 'validate' => 'validateInteger', - 'map_name' => 'OriginalImageWidth', + 'mode' => XMPReader::MODE_SIMPLE, + 'validate' => 'validateInteger', + 'map_name' => 'OriginalImageWidth', ), // LocationShown and LocationCreated are handled // specially because they are hierarchical, but we // also want to merge with the old non-hierarchical. 'LocationShown' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_BAGSTRUCT, - 'children' => array( + 'mode' => XMPReader::MODE_BAGSTRUCT, + 'children' => array( 'WorldRegion' => true, 'CountryCode' => true, /* iso code */ 'CountryName' => true, @@ -1105,8 +1111,8 @@ class XMPInfo { ), 'LocationCreated' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_BAGSTRUCT, - 'children' => array( + 'mode' => XMPReader::MODE_BAGSTRUCT, + 'children' => array( 'WorldRegion' => true, 'CountryCode' => true, /* iso code */ 'CountryName' => true, @@ -1117,35 +1123,35 @@ class XMPInfo { ), 'WorldRegion' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CountryCode' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'CountryName' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, - 'map_name' => 'Country', + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, + 'map_name' => 'Country', ), 'ProvinceState' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, - 'map_name' => 'ProvinceOrState', + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, + 'map_name' => 'ProvinceOrState', ), 'City' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), 'Sublocation' => array( 'map_group' => 'special', - 'mode' => XMPReader::MODE_SIMPLE, - 'structPart'=> true, + 'mode' => XMPReader::MODE_SIMPLE, + 'structPart' => true, ), /* Other props that might be interesting but diff --git a/includes/media/XMPValidate.php b/includes/media/XMPValidate.php index 87f8abfe..0fa60117 100644 --- a/includes/media/XMPValidate.php +++ b/includes/media/XMPValidate.php @@ -28,7 +28,7 @@ * Each of these functions take the same parameters * * an info array which is a subset of the XMPInfo::items array * * A value (passed as reference) to validate. This can be either a - * simple value or an array + * simple value or an array * * A boolean to determine if this is validating a simple or complex values * * It should be noted that when an array is being validated, typically the validation @@ -42,11 +42,11 @@ */ class XMPValidate { /** - * function to validate boolean properties ( True or False ) + * Function to validate boolean properties ( True or False ) * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateBoolean( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -57,15 +57,14 @@ class XMPValidate { wfDebugLog( 'XMP', __METHOD__ . " Expected True or False but got $val" ); $val = null; } - } /** * function to validate rational properties ( 12/10 ) * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateRational( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -76,7 +75,6 @@ class XMPValidate { wfDebugLog( 'XMP', __METHOD__ . " Expected rational but got $val" ); $val = null; } - } /** @@ -85,9 +83,9 @@ class XMPValidate { * if its outside of range put it into range. * * @see MWG spec - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateRating( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -99,6 +97,7 @@ class XMPValidate { ) { wfDebugLog( 'XMP', __METHOD__ . " Expected rating but got $val" ); $val = null; + return; } else { $nVal = (float)$val; @@ -108,11 +107,13 @@ class XMPValidate { // as -1 is meant as a special reject rating. wfDebugLog( 'XMP', __METHOD__ . " Rating too low, setting to -1 (Rejected)" ); $val = '-1'; + return; } if ( $nVal > 5 ) { wfDebugLog( 'XMP', __METHOD__ . " Rating too high, setting to 5" ); $val = '5'; + return; } } @@ -121,9 +122,9 @@ class XMPValidate { /** * function to validate integers * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateInteger( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -134,16 +135,15 @@ class XMPValidate { wfDebugLog( 'XMP', __METHOD__ . " Expected integer but got $val" ); $val = null; } - } /** * function to validate properties with a fixed number of allowed * choices. (closed choice) * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateClosed( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -171,9 +171,9 @@ class XMPValidate { /** * function to validate and modify flash structure * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateFlash( $info, &$val, $standalone ) { if ( $standalone ) { @@ -205,9 +205,9 @@ class XMPValidate { * @see rfc 3066 * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart1.pdf page 30 (section 8.2.2.5) * - * @param array $info information about current property - * @param &$val Mixed current value to validate - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate + * @param bool $standalone If this is a simple property or array */ public static function validateLangCode( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -219,7 +219,6 @@ class XMPValidate { wfDebugLog( 'XMP', __METHOD__ . " Expected Lang code but got $val" ); $val = null; } - } /** @@ -233,11 +232,11 @@ class XMPValidate { * YYYY-MM-DDThh:mm:ssTZD * YYYY-MM-DDThh:mm:ss.sTZD * - * @param array $info information about current property - * @param &$val Mixed current value to validate. Converts to TS_EXIF as a side-effect. - * in cases where there's only a partial date, it will give things like - * 2011:04. - * @param $standalone Boolean if this is a simple property or array + * @param array $info Information about current property + * @param mixed &$val Current value to validate. Converts to TS_EXIF as a side-effect. + * in cases where there's only a partial date, it will give things like + * 2011:04. + * @param bool $standalone If this is a simple property or array */ public static function validateDate( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -245,11 +244,14 @@ class XMPValidate { return; } $res = array(); + // @codingStandardsIgnoreStart Long line that cannot be broken if ( !preg_match( /* ahh! scary regex... */ '/^([0-3]\d{3})(?:-([01]\d)(?:-([0-3]\d)(?:T([0-2]\d):([0-6]\d)(?::([0-6]\d)(?:\.\d+)?)?([-+]\d{2}:\d{2}|Z)?)?)?)?$/D', $val, $res ) ) { + // @codingStandardsIgnoreEnd + wfDebugLog( 'XMP', __METHOD__ . " Expected date but got $val" ); $val = null; } else { @@ -270,6 +272,7 @@ class XMPValidate { if ( $res[1] === '0000' ) { wfDebugLog( 'XMP', __METHOD__ . " Invalid date (year 0): $val" ); $val = null; + return; } @@ -282,6 +285,7 @@ class XMPValidate { if ( isset( $res[3] ) ) { $val .= ':' . $res[3]; } + return; } @@ -292,6 +296,7 @@ class XMPValidate { if ( isset( $res[6] ) && $res[6] !== '' ) { $val .= ':' . $res[6]; } + return; } @@ -320,7 +325,6 @@ class XMPValidate { $val = substr( $val, 0, -3 ); } } - } /** function to validate, and more importantly @@ -330,10 +334,10 @@ class XMPValidate { * @see http://www.adobe.com/devnet/xmp/pdfs/XMPSpecificationPart2.pdf * section 1.2.7.4 on page 23 * - * @param array $info unused (info about prop) - * @param &$val String GPS string in either DDD,MM,SSk or - * or DDD,MM.mmk form - * @param $standalone Boolean if its a simple prop (should always be true) + * @param array $info Unused (info about prop) + * @param string &$val GPS string in either DDD,MM,SSk or + * or DDD,MM.mmk form + * @param bool $standalone If its a simple prop (should always be true) */ public static function validateGPS( $info, &$val, $standalone ) { if ( !$standalone ) { @@ -352,6 +356,7 @@ class XMPValidate { $coord = -$coord; } $val = $coord; + return; } elseif ( preg_match( '/(\d{1,3}),(\d{1,2}(?:.\d*)?)([NWSE])/D', @@ -363,12 +368,13 @@ class XMPValidate { $coord = -$coord; } $val = $coord; - return; + return; } else { wfDebugLog( 'XMP', __METHOD__ . " Expected GPSCoordinate, but got $val." ); $val = null; + return; } } |