From 2cbd926e3493ac065547aa4e9b0b4035f1d192b9 Mon Sep 17 00:00:00 2001 From: Brian Wolff Date: Wed, 28 Aug 2013 17:09:07 -0600 Subject: [PATCH] Add an interface for getting "standard" file metadata. Currently file metadata is handler dependant. However they usually end up extracting the same type of data (author, date, etc) plus one or two handler specific things. This adds a handler independent interface for getting metadata that is likely to be common for all types of file (At the moment, this is the exif/iptc/xmp information) This commit used to also contain stuff adding parser functions, which is now split to its own commit. This commit is needed by a bunch of other commits, in particular I0d957891e0. Change-Id: I43d9252f69dc5b8ba0b848cf40aa1b97329c85ae --- includes/filerepo/file/File.php | 11 + includes/media/ExifBitmap.php | 23 +- includes/media/GIF.php | 27 +- includes/media/MediaHandler.php | 33 + includes/media/PNG.php | 28 +- includes/media/SVG.php | 50 +- tests/phpunit/data/media/README | 5 + tests/phpunit/data/media/Tux.svg | 902 ++++++++++++++++++ tests/phpunit/includes/media/GIFTest.php | 35 + tests/phpunit/includes/media/JpegTest.php | 42 +- tests/phpunit/includes/media/PNGTest.php | 22 + .../media/SVGMetadataExtractorTest.php | 11 + tests/phpunit/includes/media/SVGTest.php | 51 + 13 files changed, 1201 insertions(+), 39 deletions(-) mode change 100644 => 100755 includes/media/MediaHandler.php create mode 100644 tests/phpunit/data/media/Tux.svg create mode 100644 tests/phpunit/includes/media/SVGTest.php diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index ec5f927b16..5a5221f9bc 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -512,6 +512,17 @@ abstract class File { return false; } + /** + * Like getMetadata but returns a handler independent array of common values. + * @see MediaHandler::getCommonMetaArray() + * @return Array or false if not supported + * @since 1.23 + */ + public function getCommonMetaArray() { + $handler = $this->getHandler(); + return $handler->getCommonMetaArray( $this ); + } + /** * get versioned metadata * diff --git a/includes/media/ExifBitmap.php b/includes/media/ExifBitmap.php index d8d0bede44..e4625f3ffb 100644 --- a/includes/media/ExifBitmap.php +++ b/includes/media/ExifBitmap.php @@ -117,25 +117,32 @@ class ExifBitmapHandler extends BitmapHandler { * @return array|bool */ function formatMetadata( $image ) { - $metadata = $image->getMetadata(); + $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( $image, $metadata ) === self::METADATA_BAD ) + $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 ) { diff --git a/includes/media/GIF.php b/includes/media/GIF.php index 608fb257f2..19635dacbd 100644 --- a/includes/media/GIF.php +++ b/includes/media/GIF.php @@ -47,20 +47,31 @@ class GIFHandler extends BitmapHandler { * @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']['_MW_GIF_VERSION'] ) ) { - unset( $meta['metadata']['_MW_GIF_VERSION'] ); + if ( !isset( $meta['metadata'] ) ) { + return array(); } - return $this->formatMetadataHelper( $meta['metadata'] ); + unset( $meta['metadata']['_MW_GIF_VERSION'] ); + return $meta['metadata']; } /** diff --git a/includes/media/MediaHandler.php b/includes/media/MediaHandler.php old mode 100644 new mode 100755 index 779e23c9c7..fd5b21cb26 --- a/includes/media/MediaHandler.php +++ b/includes/media/MediaHandler.php @@ -187,6 +187,39 @@ abstract class MediaHandler { return self::METADATA_GOOD; } + /** + * 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. + * + * 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 or 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. diff --git a/includes/media/PNG.php b/includes/media/PNG.php index 98f138616d..d2c17efd4f 100644 --- a/includes/media/PNG.php +++ b/includes/media/PNG.php @@ -52,20 +52,32 @@ class PNGHandler extends BitmapHandler { * @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 $image File + * @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']['_MW_PNG_VERSION'] ) ) { - unset( $meta['metadata']['_MW_PNG_VERSION'] ); + if ( !isset( $meta['metadata'] ) ) { + return array(); } - return $this->formatMetadataHelper( $meta['metadata'] ); + unset( $meta['metadata']['_MW_PNG_VERSION'] ); + return $meta['metadata']; } /** diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 72a9696c29..d6f8483eb7 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -29,6 +29,18 @@ class SvgHandler extends ImageHandler { const SVG_METADATA_VERSION = 2; + /** + * 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] ) ) { @@ -340,19 +352,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; @@ -368,7 +372,6 @@ class SvgHandler extends ImageHandler { return $showMeta ? $result : false; } - /** * @param string $name Parameter name * @param $string $value Parameter value @@ -431,4 +434,29 @@ class SvgHandler extends ImageHandler { 'lang' => $params['lang'], ); } + + 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/tests/phpunit/data/media/README b/tests/phpunit/data/media/README index fe3bc68299..4b489a92c9 100644 --- a/tests/phpunit/data/media/README +++ b/tests/phpunit/data/media/README @@ -36,3 +36,8 @@ http://commons.wikimedia.org/wiki/File:Animated_PNG_example_bouncing_beach_ball. Public Domain Holger Will +Tux.svg +https://commons.wikimedia.org/wiki/File:Tux.svg +Larry Ewing, Simon Budig, Anja Gerwinski +"The copyright holder of this file allows anyone to use it for any purpose, provided that the copyright holder is properly attributed. Redistribution, derivative work, commercial use, and all other use is permitted." + diff --git a/tests/phpunit/data/media/Tux.svg b/tests/phpunit/data/media/Tux.svg new file mode 100644 index 0000000000..39561078e8 --- /dev/null +++ b/tests/phpunit/data/media/Tux.svg @@ -0,0 +1,902 @@ + + + Tux + For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php index 7ea6b7ef9d..12ab6d438e 100644 --- a/tests/phpunit/includes/media/GIFTest.php +++ b/tests/phpunit/includes/media/GIFTest.php @@ -97,6 +97,41 @@ class GIFHandlerTest extends MediaWikiTestCase { ); } + /** + * @param $filename String + * @param $expected String Serialized array + * @dataProvider provideGetIndependentMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public function provideGetIndependentMetaArray() { + return array( + array( 'nonanimated.gif', array( + 'GIFFileComment' => array( + 'GIF test file ⁕ Created with GIMP', + ), + ) ), + array( 'animated-xmp.gif', + array( + 'Artist' => 'Bawolff', + 'ImageDescription' => array( + 'x-default' => 'A file to test GIF', + '_type' => 'lang', + ), + 'SublocationDest' => 'The interwebs', + 'GIFFileComment' => + array( + 'GIƒ·test·file', + ), + ) + ), + ); + } + private function dataFile( $name, $type ) { return new UnregisteredLocalFile( false, $this->repo, "mwstore://localtesting/data/$name", $type ); diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php index 7775c41733..be8cb9eaa2 100644 --- a/tests/phpunit/includes/media/JpegTest.php +++ b/tests/phpunit/includes/media/JpegTest.php @@ -11,20 +11,54 @@ class JpegTest extends MediaWikiTestCase { $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'data' => $this->filePath ) + ) ); + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ) ); + + $this->handler = new JpegHandler; } public function testInvalidFile() { - $jpeg = new JpegHandler; - $res = $jpeg->getMetadata( null, $this->filePath . 'README' ); + $file = $this->dataFile( 'README', 'image/jpeg' ); + $res = $this->handler->getMetadata( $file, $this->filePath . 'README' ); $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); } public function testJpegMetadataExtraction() { - $h = new JpegHandler; - $res = $h->getMetadata( null, $this->filePath . 'test.jpg' ); + $file = $this->dataFile( 'test.jpg', 'image/jpeg' ); + $res = $this->handler->getMetadata( $file, $this->filePath . 'test.jpg' ); $expected = 'a:7:{s:16:"ImageDescription";s:9:"Test file";s:11:"XResolution";s:4:"72/1";s:11:"YResolution";s:4:"72/1";s:14:"ResolutionUnit";i:2;s:16:"YCbCrPositioning";i:1;s:15:"JPEGFileComment";a:1:{i:0;s:17:"Created with GIMP";}s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; // Unserialize in case serialization format ever changes. $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); } + public function testGetIndependentMetaArray() { + $file = $this->dataFile( 'test.jpg', 'image/jpeg' ); + $res = $this->handler->getCommonMetaArray( $file ); + $expected = array( + 'ImageDescription' => 'Test file', + 'XResolution' => '72/1', + 'YResolution' => '72/1', + 'ResolutionUnit' => 2, + 'YCbCrPositioning' => 1, + 'JPEGFileComment' => array( + 'Created with GIMP', + ), + ); + + $this->assertEquals( $res, $expected ); + } + + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } } diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php index 855780da77..1c642c8d6b 100644 --- a/tests/phpunit/includes/media/PNGTest.php +++ b/tests/phpunit/includes/media/PNGTest.php @@ -100,6 +100,28 @@ class PNGHandlerTest extends MediaWikiTestCase { ); } + /** + * @param $filename String + * @param $expected Array Expected standard metadata + * @dataProvider provideGetIndependentMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public function provideGetIndependentMetaArray() { + return array( + array( 'rgb-na-png.png', array() ), + array( 'xmp.png', + array( + 'SerialNumber' => '123456789', + ) + ), + ); + } + private function dataFile( $name, $type ) { return new UnregisteredLocalFile( false, $this->repo, "mwstore://localtesting/data/$name", $type ); diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php index 3bf9c59ff8..7f01fd8746 100644 --- a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -80,6 +80,17 @@ class SVGMetadataExtractorTest extends MediaWikiTestCase { 'originalWidth' => '385', 'originalHeight' => '385.0004883', ) + ), + array( + "$base/Tux.svg", + array( + 'width' => 512, + 'height' => 594, + 'originalWidth' => '100%', + 'originalHeight' => '100%', + 'title' => 'Tux', + 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ) ) ); } diff --git a/tests/phpunit/includes/media/SVGTest.php b/tests/phpunit/includes/media/SVGTest.php new file mode 100644 index 0000000000..796975697a --- /dev/null +++ b/tests/phpunit/includes/media/SVGTest.php @@ -0,0 +1,51 @@ +filePath = __DIR__ . '/../../data/media/'; + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'lockManager' => 'nullLockManager', + 'containerPaths' => array( 'data' => $this->filePath ) + ) ); + $this->repo = new FSRepo( array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ) ); + + $this->handler = new SVGHandler; + } + + /** + * @param $filename String + * @param $expected Array The expected independent metadata + * @dataProvider providerGetIndependentMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/svg+xml' ); + $res = $this->handler->getCommonMetaArray( $file ); + + $this->assertEquals( $res, $expected ); + } + + public function providerGetIndependentMetaArray() { + return array( + array( 'Tux.svg', array( + 'ObjectName' => 'Tux', + 'ImageDescription' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ) ), + array( 'Wikimedia-logo.svg', array() ) + ); + } + + private function dataFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} -- 2.20.1