diff options
Diffstat (limited to 'tests/phpunit/includes/media')
23 files changed, 2479 insertions, 0 deletions
diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php new file mode 100644 index 00000000..c720d7b7 --- /dev/null +++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php @@ -0,0 +1,167 @@ +<?php + +/** + * @group Media + */ +class BitmapMetadataHandlerTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( 'wgShowEXIF', false ); + + $this->filePath = __DIR__ . '/../../data/media/'; + } + + /** + * Test if having conflicting metadata values from different + * types of metadata, that the right one takes precedence. + * + * Basically the file has IPTC and XMP metadata, the + * IPTC should override the XMP, except for the multilingual + * translation (to en) where XMP should win. + * @covers BitmapMetadataHandler::Jpeg + */ + public function testMultilingualCascade() { + $this->checkPHPExtension( 'exif' ); + $this->checkPHPExtension( 'xml' ); + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + '/Xmp-exif-multilingual_test.jpg' ); + + $expected = array( + 'x-default' => 'right(iptc)', + 'en' => 'right translation', + '_type' => 'lang' + ); + + $this->assertArrayHasKey( 'ImageDescription', $meta, + 'Did not extract any ImageDescription info?!' ); + + $this->assertEquals( $expected, $meta['ImageDescription'] ); + } + + /** + * Test for jpeg comments are being handled by + * BitmapMetadataHandler correctly. + * + * There's more extensive tests of comment extraction in + * JpegMetadataExtractorTests.php + * @covers BitmapMetadataHandler::Jpeg + */ + public function testJpegComment() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'jpeg-comment-utf.jpg' ); + + $this->assertEquals( 'UTF-8 JPEG Comment — ¼', + $meta['JPEGFileComment'][0] ); + } + + /** + * Make sure a bad iptc block doesn't stop the other metadata + * from being extracted. + * @covers BitmapMetadataHandler::Jpeg + */ + public function testBadIPTC() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-invalid-psir.jpg' ); + $this->assertEquals( 'Created with GIMP', $meta['JPEGFileComment'][0] ); + } + + /** + * @covers BitmapMetadataHandler::Jpeg + */ + public function testIPTCDates() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest.jpg' ); + + $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] ); + $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] ); + } + + /** + * File has an invalid time (+ one valid but really weird time) + * that shouldn't be included + * @covers BitmapMetadataHandler::Jpeg + */ + public function testIPTCDatesInvalid() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest-invalid.jpg' ); + + $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] ); + $this->assertFalse( isset( $meta['DateTimeDigitized'] ) ); + } + + /** + * XMP data should take priority over iptc data + * when hash has been updated, but not when + * the hash is wrong. + * @covers BitmapMetadataHandler::addMetadata + * @covers BitmapMetadataHandler::getMetadataArray + */ + public function testMerging() { + $merger = new BitmapMetadataHandler(); + $merger->addMetadata( array( 'foo' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'bar' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'baz' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'fred' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'foo' => 'iptc (hash)' ), 'iptc-good-hash' ); + $merger->addMetadata( array( 'bar' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'baz' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'fred' => 'iptc (no hash)' ), 'iptc-no-hash' ); + $merger->addMetadata( array( 'baz' => 'exif' ), 'exif' ); + + $actual = $merger->getMetadataArray(); + $expected = array( + 'foo' => 'xmp', + 'bar' => 'iptc (bad hash)', + 'baz' => 'exif', + 'fred' => 'xmp', + ); + $this->assertEquals( $expected, $actual ); + } + + /** + * @covers BitmapMetadataHandler::png + */ + public function testPNGXMP() { + if ( !extension_loaded( 'xml' ) ) { + $this->markTestSkipped( "This test needs the xml extension." ); + } + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'xmp.png' ); + $expected = array( + 'frameCount' => 0, + 'loopCount' => 1, + 'duration' => 0, + 'bitDepth' => 1, + 'colorType' => 'index-coloured', + 'metadata' => array( + 'SerialNumber' => '123456789', + '_MW_PNG_VERSION' => 1, + ), + ); + $this->assertEquals( $expected, $result ); + } + + /** + * @covers BitmapMetadataHandler::png + */ + public function testPNGNative() { + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'Png-native-test.png' ); + $expected = 'http://example.com/url'; + $this->assertEquals( $expected, $result['metadata']['Identifier']['x-default'] ); + } + + /** + * @covers BitmapMetadataHandler::getTiffByteOrder + */ + public function testTiffByteOrder() { + $handler = new BitmapMetadataHandler(); + $res = $handler->getTiffByteOrder( $this->filePath . 'test.tiff' ); + $this->assertEquals( 'LE', $res ); + } +} diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php new file mode 100644 index 00000000..1972c969 --- /dev/null +++ b/tests/phpunit/includes/media/BitmapScalingTest.php @@ -0,0 +1,140 @@ +<?php + +/** + * @group Media + */ +class BitmapScalingTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgMaxImageArea' => 1.25e7, // 3500x3500 + 'wgCustomConvertCommand' => 'dummy', // Set so that we don't get client side rendering + ) ); + } + + /** + * @dataProvider provideNormaliseParams + * @covers BitmapHandler::normaliseParams + */ + public function testNormaliseParams( $fileDimensions, $expectedParams, $params, $msg ) { + $file = new FakeDimensionFile( $fileDimensions ); + $handler = new BitmapHandler; + $valid = $handler->normaliseParams( $file, $params ); + $this->assertTrue( $valid ); + $this->assertEquals( $expectedParams, $params, $msg ); + } + + public static function provideNormaliseParams() { + return array( + /* Regular resize operations */ + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512 ), + 'Resizing with width set', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512, 'height' => 768 ), + 'Resizing with height set too high', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 1024, 'height' => 384 ), + 'Resizing with height set', + ), + + /* Very tall images */ + array( + array( 1000, 100 ), + array( + 'width' => 5, 'height' => 1, + 'physicalWidth' => 5, 'physicalHeight' => 1, + 'page' => 1, + ), + array( 'width' => 5 ), + 'Very wide image', + ), + + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 10, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 1 ), + 'Very high image', + ), + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 5, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 10, 'height' => 5 ), + 'Very high image with height set', + ), + /* Max image area */ + array( + array( 4000, 4000 ), + array( + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, + ), + array( 'width' => 5000 ), + 'Bigger than max image size but doesn\'t need scaling', + ), + ); + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $handler = new BitmapHandler; + $params = array( 'width' => '3700' ); // Still bigger than max size. + $this->assertEquals( 'TransformParameterError', + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigMustRenderImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $file->mustRender = true; + $handler = new BitmapHandler; + $params = array( 'width' => '5000' ); // Still bigger than max size. + $this->assertEquals( 'TransformParameterError', + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::getImageArea + */ + public function testImageArea() { + $file = new FakeDimensionFile( array( 7, 9 ) ); + $handler = new BitmapHandler; + $this->assertEquals( 63, $handler->getImageArea( $file ) ); + } +} diff --git a/tests/phpunit/includes/media/DjVuTest.php b/tests/phpunit/includes/media/DjVuTest.php new file mode 100644 index 00000000..c0871f19 --- /dev/null +++ b/tests/phpunit/includes/media/DjVuTest.php @@ -0,0 +1,69 @@ +<?php +/** + * @group Media + * @covers DjVuHandler + */ +class DjVuTest extends MediaWikiMediaTestCase { + + /** + * @var DjVuHandler + */ + protected $handler; + + protected function setUp() { + parent::setUp(); + + //cli tool setup + $djvuSupport = new DjVuSupport(); + + if ( !$djvuSupport->isEnabled() ) { + $this->markTestSkipped( + 'This test needs the installation of the ddjvu, djvutoxml and djvudump tools' ); + } + + $this->handler = new DjVuHandler(); + } + + public function testGetImageSize() { + $this->assertArrayEquals( + array( 2480, 3508, 'DjVu', 'width="2480" height="3508"' ), + $this->handler->getImageSize( null, $this->filePath . '/LoremIpsum.djvu' ), + 'Test file LoremIpsum.djvu should have a size of 2480 * 3508' + ); + } + + public function testInvalidFile() { + $this->assertEquals( + 'a:1:{s:5:"error";s:25:"Error extracting metadata";}', + $this->handler->getMetadata( null, $this->filePath . '/some-nonexistent-file' ), + 'Getting metadata for an inexistent file should return false' + ); + } + + public function testPageCount() { + $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' ); + $this->assertEquals( + 5, + $this->handler->pageCount( $file ), + 'Test file LoremIpsum.djvu should be detected as containing 5 pages' + ); + } + + public function testGetPageDimensions() { + $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' ); + $this->assertArrayEquals( + array( 2480, 3508 ), + $this->handler->getPageDimensions( $file, 1 ), + 'Page 1 of test file LoremIpsum.djvu should have a size of 2480 * 3508' + ); + } + + public function testGetPageText() { + $file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' ); + $this->assertEquals( + "Lorem ipsum \n1 \n", + (string)$this->handler->getPageText( $file, 1 ), + "Text layer of page 1 of file LoremIpsum.djvu should be 'Lorem ipsum \n1 \n'" + ); + } +} diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php new file mode 100644 index 00000000..41330f41 --- /dev/null +++ b/tests/phpunit/includes/media/ExifBitmapTest.php @@ -0,0 +1,146 @@ +<?php + +/** + * @group Media + */ +class ExifBitmapTest extends MediaWikiTestCase { + + /** + * @var ExifBitmapHandler + */ + protected $handler; + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->handler = new ExifBitmapHandler; + + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsOldBroken() { + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsBrokenFile() { + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsInvalid() { + $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testGoodMetadata() { + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + // @codingStandardsIgnoreEnd + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsOldGood() { + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}'; + // @codingStandardsIgnoreEnd + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + + /** + * Handle metadata from paged tiff handler (gotten via instant commons) gracefully. + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testPagedTiffHandledGracefully() { + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $meta = 'a:6:{s:9:"page_data";a:1:{i:1;a:5:{s:5:"width";i:643;s:6:"height";i:448;s:5:"alpha";s:4:"true";s:4:"page";i:1;s:6:"pixels";i:288064;}}s:10:"page_count";i:1;s:10:"first_page";i:1;s:9:"last_page";i:1;s:4:"exif";a:9:{s:10:"ImageWidth";i:643;s:11:"ImageLength";i:448;s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:4;s:12:"RowsPerStrip";i:50;s:19:"PlanarConfiguration";i:1;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}s:21:"TIFF_METADATA_VERSION";s:3:"1.4";}'; + // @codingStandardsIgnoreEnd + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataLatest() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'MEDIAWIKI_EXIF_VERSION' => 2 + ); + $res = $this->handler->convertMetadataVersion( $metadata, 2 ); + $this->assertEquals( $metadata, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataToOld() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'bar' => array( 'First', 'Second', '_type' => 'ul' ), + 'baz' => array( 'First', 'Second' ), + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'foo' => "\n#First\n#Second", + 'bar' => "\n*First\n*Second", + 'baz' => "\n*First\n*Second", + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataSoftware() { + $metadata = array( + 'Software' => array( array( 'GIMP', '1.1' ) ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => 'GIMP (Version 1.1)', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataSoftwareNormal() { + $metadata = array( + 'Software' => array( "GIMP 1.2", "vim" ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => "\n*GIMP 1.2\n*vim", + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } +} diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php new file mode 100644 index 00000000..f0bd42a0 --- /dev/null +++ b/tests/phpunit/includes/media/ExifRotationTest.php @@ -0,0 +1,280 @@ +<?php +/** + * Tests related to auto rotation. + * + * @group Media + * @group medium + * + * @todo covers tags + */ +class ExifRotationTest extends MediaWikiMediaTestCase { + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->handler = new BitmapHandler(); + + $this->setMwGlobals( array( + 'wgShowEXIF' => true, + 'wgEnableAutoRotation' => true, + ) ); + } + + /** + * Mark this test as creating thumbnail files. + */ + protected function createsThumbnails() { + return true; + } + + /** + * @dataProvider provideFiles + */ + public function testMetadata( $name, $type, $info ) { + if ( !$this->handler->canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * Same as before, but with auto-rotation set to auto. + * + * This sets scaler to image magick, which we should detect as + * supporting rotation. + * @dataProvider provideFiles + */ + public function testMetadataAutoRotate( $name, $type, $info ) { + $this->setMwGlobals( 'wgEnableAutoRotation', null ); + $this->setMwGlobals( 'wgUseImageMagick', true ); + $this->setMwGlobals( 'wgUseImageResize', true ); + + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * + * @dataProvider provideFiles + */ + public function testRotationRendering( $name, $type, $info, $thumbs ) { + if ( !$this->handler->canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + foreach ( $thumbs as $size => $out ) { + if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException( 'bogus test data format ' . $size ); + } + + $file = $this->dataFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + + $this->assertEquals( + $out[0], + $thumb->getWidth(), + "$name: thumb reported width check for $size" + ); + $this->assertEquals( + $out[1], + $thumb->getHeight(), + "$name: thumb reported height check for $size" + ); + + $gis = getimagesize( $thumb->getLocalCopyPath() ); + if ( $out[0] > $info['width'] ) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" ); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" ); + } + } + } + + public static function provideFiles() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 768, // as rotated + 'height' => 1024, // as rotated + ), + array( + '800x600px' => array( 450, 600 ), + '9999x800px' => array( 600, 800 ), + '800px' => array( 800, 1067 ), + '600px' => array( 600, 800 ), + ) + ) + ); + } + + /** + * Same as before, but with auto-rotation disabled. + * @dataProvider provideFilesNoAutoRotate + */ + public function testMetadataNoAutoRotate( $name, $type, $info ) { + $this->setMwGlobals( 'wgEnableAutoRotation', false ); + + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * Same as before, but with auto-rotation set to auto and an image scaler that doesn't support it. + * @dataProvider provideFilesNoAutoRotate + */ + public function testMetadataAutoRotateUnsupported( $name, $type, $info ) { + $this->setMwGlobals( 'wgEnableAutoRotation', null ); + $this->setMwGlobals( 'wgUseImageResize', false ); + + $file = $this->dataFile( $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * + * @dataProvider provideFilesNoAutoRotate + */ + public function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) { + $this->setMwGlobals( 'wgEnableAutoRotation', false ); + + foreach ( $thumbs as $size => $out ) { + if ( preg_match( '/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException( 'bogus test data format ' . $size ); + } + + $file = $this->dataFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + + $this->assertEquals( + $out[0], + $thumb->getWidth(), + "$name: thumb reported width check for $size" + ); + $this->assertEquals( + $out[1], + $thumb->getHeight(), + "$name: thumb reported height check for $size" + ); + + $gis = getimagesize( $thumb->getLocalCopyPath() ); + if ( $out[0] > $info['width'] ) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size" ); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size" ); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size" ); + } + } + } + + public static function provideFilesNoAutoRotate() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 1024, // since not rotated + 'height' => 768, // since not rotated + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ) + ); + } + + const TEST_WIDTH = 100; + const TEST_HEIGHT = 200; + + /** + * @dataProvider provideBitmapExtractPreRotationDimensions + */ + public function testBitmapExtractPreRotationDimensions( $rotation, $expected ) { + $result = $this->handler->extractPreRotationDimensions( array( + 'physicalWidth' => self::TEST_WIDTH, + 'physicalHeight' => self::TEST_HEIGHT, + ), $rotation ); + $this->assertEquals( $expected, $result ); + } + + public static function provideBitmapExtractPreRotationDimensions() { + return array( + array( + 0, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 90, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + array( + 180, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 270, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php new file mode 100644 index 00000000..f3c05fb1 --- /dev/null +++ b/tests/phpunit/includes/media/ExifTest.php @@ -0,0 +1,47 @@ +<?php + +/** + * @group Media + * @covers Exif + */ +class ExifTest extends MediaWikiTestCase { + + /** @var string */ + protected $mediaPath; + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->mediaPath = __DIR__ . '/../../data/media/'; + + $this->setMwGlobals( 'wgShowEXIF', true ); + } + + public function testGPSExtraction() { + $filename = $this->mediaPath . 'exif-gps.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + $expected = array( + 'GPSLatitude' => 88.5180555556, + 'GPSLongitude' => -21.12357, + 'GPSAltitude' => -3.141592653, + 'GPSDOP' => '5/1', + 'GPSVersionID' => '2.2.0.0', + ); + $this->assertEquals( $expected, $data, '', 0.0000000001 ); + } + + public function testUnicodeUserComment() { + $filename = $this->mediaPath . 'exif-user-comment.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + + $expected = array( + 'UserComment' => 'test⁔comment' + ); + $this->assertEquals( $expected, $data ); + } +} diff --git a/tests/phpunit/includes/media/FakeDimensionFile.php b/tests/phpunit/includes/media/FakeDimensionFile.php new file mode 100644 index 00000000..4b8f213e --- /dev/null +++ b/tests/phpunit/includes/media/FakeDimensionFile.php @@ -0,0 +1,31 @@ +<?php + +/** + * @group Media + */ +class FakeDimensionFile extends File { + public $mustRender = false; + + public function __construct( $dimensions ) { + parent::__construct( Title::makeTitle( NS_FILE, 'Test' ), + new NullRepo( null ) ); + + $this->dimensions = $dimensions; + } + + public function getWidth( $page = 1 ) { + return $this->dimensions[0]; + } + + public function getHeight( $page = 1 ) { + return $this->dimensions[1]; + } + + public function mustRender() { + return $this->mustRender; + } + + public function getPath() { + return ''; + } +} diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php new file mode 100644 index 00000000..002e2cb9 --- /dev/null +++ b/tests/phpunit/includes/media/FormatMetadataTest.php @@ -0,0 +1,71 @@ +<?php + +/** + * @group Media + */ +class FormatMetadataTest extends MediaWikiMediaTestCase { + + protected function setUp() { + parent::setUp(); + + $this->checkPHPExtension( 'exif' ); + $this->setMwGlobals( 'wgShowEXIF', true ); + } + + /** + * @covers File::formatMetadata + */ + public function testInvalidDate() { + $file = $this->dataFile( 'broken_exif_date.jpg', 'image/jpeg' ); + + // Throws an error if bug hit + $meta = $file->formatMetadata(); + $this->assertNotEquals( false, $meta, 'Valid metadata extracted' ); + + // Find date exif entry + $this->assertArrayHasKey( 'visible', $meta ); + $dateIndex = null; + foreach ( $meta['visible'] as $i => $data ) { + if ( $data['id'] == 'exif-datetimeoriginal' ) { + $dateIndex = $i; + } + } + $this->assertNotNull( $dateIndex, 'Date entry exists in metadata' ); + $this->assertEquals( '0000:01:00 00:02:27', + $meta['visible'][$dateIndex]['value'], + 'File with invalid date metadata (bug 29471)' ); + } + + /** + * @param string $filename + * @param int $expected Total image area + * @dataProvider provideFlattenArray + * @covers FormatMetadata::flattenArray + */ + public function testFlattenArray( $vals, $type, $noHtml, $ctx, $expected ) { + $actual = FormatMetadata::flattenArray( $vals, $type, $noHtml, $ctx ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideFlattenArray() { + return array( + array( + array( 1, 2, 3 ), 'ul', false, false, + "<ul><li>1</li>\n<li>2</li>\n<li>3</li></ul>", + ), + array( + array( 1, 2, 3 ), 'ol', false, false, + "<ol><li>1</li>\n<li>2</li>\n<li>3</li></ol>", + ), + array( + array( 1, 2, 3 ), 'ul', true, false, + "\n*1\n*2\n*3", + ), + array( + array( 1, 2, 3 ), 'ol', true, false, + "\n#1\n#2\n#3", + ), + // TODO: more test cases + ); + } +} diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php new file mode 100644 index 00000000..6aecd8b1 --- /dev/null +++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -0,0 +1,111 @@ +<?php + +/** + * @group Media + */ +class GIFMetadataExtractorTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->mediaPath = __DIR__ . '/../../data/media/'; + } + + /** + * Put in a file, and see if the metadata coming out is as expected. + * @param string $filename + * @param array $expected The extracted metadata. + * @dataProvider provideGetMetadata + * @covers GIFMetadataExtractor::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetMetadata() { + + $xmpNugget = <<<EOF +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'> + <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:tiff='http://ns.adobe.com/tiff/1.0/'> + <tiff:Artist>Bawolff</tiff:Artist> + <tiff:ImageDescription> + <rdf:Alt> + <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li> + </rdf:Alt> + </tiff:ImageDescription> + </rdf:Description> +</rdf:RDF> +</x:xmpmeta> + + + + + + + + + + + + + + + + + + + + + + + + +<?xpacket end='w'?> +EOF; + $xmpNugget = str_replace( "\r", '', $xmpNugget ); // Windows compat + + return array( + array( + 'nonanimated.gif', + array( + 'comment' => array( 'GIF test file ⁕ Created with GIMP' ), + 'duration' => 0.1, + 'frameCount' => 1, + 'looped' => false, + 'xmp' => '', + ) + ), + array( + 'animated.gif', + array( + 'comment' => array( 'GIF test file . Created with GIMP' ), + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'xmp' => '', + ) + ), + + array( + 'animated-xmp.gif', + array( + 'xmp' => $xmpNugget, + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'comment' => array( 'GIƒ·test·file' ), + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php new file mode 100644 index 00000000..87ffd995 --- /dev/null +++ b/tests/phpunit/includes/media/GIFTest.php @@ -0,0 +1,142 @@ +<?php + +/** + * @group Media + */ +class GIFHandlerTest extends MediaWikiMediaTestCase { + + /** @var GIFHandler */ + protected $handler; + + protected function setUp() { + parent::setUp(); + + $this->handler = new GIFHandler(); + } + + /** + * @covers GIFHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( GIFHandler::BROKEN_FILE, $res ); + } + + /** + * @param string $filename Basename of the file to check + * @param bool $expected Expected result. + * @dataProvider provideIsAnimated + * @covers GIFHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return array( + array( 'animated.gif', true ), + array( 'nonanimated.gif', false ), + ); + } + + /** + * @param string $filename + * @param int $expected Total image area + * @dataProvider provideGetImageArea + * @covers GIFHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return array( + array( 'animated.gif', 5400 ), + array( 'nonanimated.gif', 1350 ), + ); + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of GIFHandler + * @dataProvider provideIsMetadataValid + * @covers GIFHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return array( + array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ), + array( '', GIFHandler::METADATA_BAD ), + array( null, GIFHandler::METADATA_BAD ), + array( 'Something invalid!', GIFHandler::METADATA_BAD ), + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + array( 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', GIFHandler::METADATA_GOOD ), + // @codingStandardsIgnoreEnd + ); + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers GIFHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); + $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + } + + public static function provideGetMetadata() { + return array( + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + array( 'nonanimated.gif', 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + array( 'animated-xmp.gif', 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + // @codingStandardsIgnoreEnd + ); + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetIndependentMetaArray + * @covers GIFHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static 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', + ), + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php new file mode 100644 index 00000000..06542cfe --- /dev/null +++ b/tests/phpunit/includes/media/IPTCTest.php @@ -0,0 +1,85 @@ +<?php + +/** + * @group Media + */ +class IPTCTest extends MediaWikiTestCase { + + /** + * @covers IPTC::getCharset + */ + public function testRecognizeUtf8() { + // utf-8 is the only one used in practise. + $res = IPTC::getCharset( "\x1b%G" ); + $this->assertEquals( 'UTF-8', $res ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharset88591() { + // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1 + // This data doesn't specify a charset. We're supposed to guess + // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not) + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharset88591b() { + /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */ + /* \xC3 = Ã, \xB8 = ¸ */ + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ÃÃø' ), $res['Keywords'] ); + } + + /** + * Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8. + * What should happen is the first "\xC3\xC3" should be dropped as invalid, + * leaving \xC3\xB8, which is ø + * @covers IPTC::Parse + */ + public function testIPTCParseForcedUTFButInvalid() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8" + . "\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ø' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseNoCharsetUTF8() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + + /** + * Testing something that has 2 values for keyword + * @covers IPTC::Parse + */ + public function testIPTCParseMulti() { + $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4" + /* length */ . "\0\0\0\0\0\x0D" + . "\x1c\x02\x19" . "\x00\x01" . "\xBC" + . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼', '¼½' ), $res['Keywords'] ); + } + + /** + * @covers IPTC::Parse + */ + public function testIPTCParseUTF8() { + // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8. + $iptcData = + "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } +} diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php new file mode 100644 index 00000000..7c977d5a --- /dev/null +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -0,0 +1,111 @@ +<?php +/** + * @todo Could use a test of extended XMP segments. Hard to find programs that + * create example files, and creating my own in vim propbably wouldn't + * serve as a very good "test". (Adobe photoshop probably creates such files + * but it costs money). The implementation of it currently in MediaWiki is based + * solely on reading the standard, without any real world test files. + * + * @group Media + * @covers JpegMetadataExtractor + */ +class JpegMetadataExtractorTest extends MediaWikiTestCase { + + protected $filePath; + + protected function setUp() { + parent::setUp(); + + $this->filePath = __DIR__ . '/../../data/media/'; + } + + /** + * We also use this test to test padding bytes don't + * screw stuff up + * + * @param string $file Filename + * + * @dataProvider provideUtf8Comment + */ + public function testUtf8Comment( $file ) { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file ); + $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] ); + } + + public static function provideUtf8Comment() { + return array( + array( 'jpeg-comment-utf.jpg' ), + array( 'jpeg-padding-even.jpg' ), + array( 'jpeg-padding-odd.jpg' ), + ); + } + + /** The file is iso-8859-1, but it should get auto converted */ + public function testIso88591Comment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' ); + $this->assertEquals( array( 'ISO-8859-1 JPEG Comment - ¼' ), $res['COM'] ); + } + + /** Comment values that are non-textual (random binary junk) should not be shown. + * The example test file has a comment with a 0x5 byte in it which is a control character + * and considered binary junk for our purposes. + */ + public function testBinaryCommentStripped() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' ); + $this->assertEmpty( $res['COM'] ); + } + + /* Very rarely a file can have multiple comments. + * Order of comments is based on order inside the file. + */ + public function testMultipleComment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' ); + $this->assertEquals( array( 'foo', 'bar' ), $res['COM'] ); + } + + public function testXMPExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + + public function testPSIRExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = '50686f746f73686f7020332e30003842494d04040000000' + . '000181c02190004746573741c02190003666f6f1c020000020004'; + $this->assertEquals( $expected, bin2hex( $res['PSIR'][0] ) ); + } + + public function testXMPExtractionAltAppId() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + + public function testIPTCHashComparisionNoHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-no-hash', $res ); + } + + public function testIPTCHashComparisionBadHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-bad-hash', $res ); + } + + public function testIPTCHashComparisionGoodHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'][0] ); + + $this->assertEquals( 'iptc-good-hash', $res ); + } + + public function testExifByteOrder() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' ); + $expected = 'BE'; + $this->assertEquals( $expected, $res['byteOrder'] ); + } +} diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php new file mode 100644 index 00000000..2436e7d9 --- /dev/null +++ b/tests/phpunit/includes/media/JpegTest.php @@ -0,0 +1,54 @@ +<?php + +/** + * @group Media + * @covers JpegHandler + */ +class JpegTest extends MediaWikiMediaTestCase { + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->handler = new JpegHandler; + } + + public function testInvalidFile() { + $file = $this->dataFile( 'README', 'image/jpeg' ); + $res = $this->handler->getMetadata( $file, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + + public function testJpegMetadataExtraction() { + $file = $this->dataFile( 'test.jpg', 'image/jpeg' ); + $res = $this->handler->getMetadata( $file, $this->filePath . 'test.jpg' ); + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $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;}'; + // @codingStandardsIgnoreEnd + + // Unserialize in case serialization format ever changes. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } + + /** + * @covers JpegHandler::getCommonMetaArray + */ + 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 ); + } +} diff --git a/tests/phpunit/includes/media/MediaHandlerTest.php b/tests/phpunit/includes/media/MediaHandlerTest.php new file mode 100644 index 00000000..d8cfcc45 --- /dev/null +++ b/tests/phpunit/includes/media/MediaHandlerTest.php @@ -0,0 +1,56 @@ +<?php + +/** + * @group Media + */ +class MediaHandlerTest extends MediaWikiTestCase { + + /** + * @covers MediaHandler::fitBoxWidth + * @todo split into a dataprovider and test method + */ + public function testFitBoxWidth() { + $vals = array( + array( + 'width' => 50, + 'height' => 50, + 'tests' => array( + 50 => 50, + 17 => 17, + 18 => 18 ) ), + array( + 'width' => 366, + 'height' => 300, + 'tests' => array( + 50 => 61, + 17 => 21, + 18 => 22 ) ), + array( + 'width' => 300, + 'height' => 366, + 'tests' => array( + 50 => 41, + 17 => 14, + 18 => 15 ) ), + array( + 'width' => 100, + 'height' => 400, + 'tests' => array( + 50 => 12, + 17 => 4, + 18 => 4 ) ) ); + foreach ( $vals as $row ) { + $tests = $row['tests']; + $height = $row['height']; + $width = $row['width']; + foreach ( $tests as $max => $expected ) { + $y = round( $expected * $height / $width ); + $result = MediaHandler::fitBoxWidth( $width, $height, $max ); + $y2 = round( $result * $height / $width ); + $this->assertEquals( $expected, + $result, + "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" ); + } + } + } +} diff --git a/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php new file mode 100644 index 00000000..8f28158d --- /dev/null +++ b/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -0,0 +1,86 @@ +<?php +/** + * Specificly for testing Media handlers. Sets up a FSFile backend + */ +abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { + + /** @var FSRepo */ + protected $repo; + /** @var FSFileBackend */ + protected $backend; + /** @var string */ + protected $filePath; + + + protected function setUp() { + parent::setUp(); + + $this->filePath = $this->getFilePath(); + $containers = array( 'data' => $this->filePath ); + if ( $this->createsThumbnails() ) { + // We need a temp directory for the thumbnails + // the container is named 'temp-thumb' because it is the + // thumb directory for a FSRepo named "temp". + $containers['temp-thumb'] = $this->getNewTempDirectory(); + } + + $this->backend = new FSFileBackend( array( + 'name' => 'localtesting', + 'wikiId' => wfWikiId(), + 'containerPaths' => $containers + ) ); + $this->repo = new FSRepo( $this->getRepoOptions() ); + } + + /** + * @return array Argument for FSRepo constructor + */ + protected function getRepoOptions() { + return array( + 'name' => 'temp', + 'url' => 'http://localhost/thumbtest', + 'backend' => $this->backend + ); + } + + /** + * The result of this method will set the file path to use, + * as well as the protected member $filePath + * + * @return string Path where files are + */ + protected function getFilePath() { + return __DIR__ . '/../../data/media/'; + } + + /** + * Will the test create thumbnails (and thus do we need to set aside + * a temporary directory for them?) + * + * Override this method if your test case creates thumbnails + * + * @return bool + */ + protected function createsThumbnails() { + return false; + } + + /** + * Utility function: Get a new file object for a file on disk but not actually in db. + * + * File must be in the path returned by getFilePath() + * @param string $name File name + * @param string $type MIME type [optional] + * @return UnregisteredLocalFile + */ + protected function dataFile( $name, $type = null ) { + if ( !$type ) { + // Autodetect by file extension for the lazy. + $magic = MimeMagic::singleton(); + $parts = explode( $name, '.' ); + $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] ); + } + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php new file mode 100644 index 00000000..a9eaa9e7 --- /dev/null +++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -0,0 +1,155 @@ +<?php + +/** + * @group Media + * @covers PNGMetadataExtractor + */ +class PNGMetadataExtractorTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + $this->filePath = __DIR__ . '/../../data/media/'; + } + + /** + * Tests zTXt tag (compressed textual metadata) + */ + public function testPngNativetZtxt() { + $this->checkPHPExtension( 'zlib' ); + + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "foo bar baz foo foo foo foof foo foo foo foo"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Make', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Make'] ); + + $this->assertEquals( $expected, $meta['Make']['x-default'] ); + } + + /** + * Test tEXt tag (Uncompressed textual metadata) + */ + public function testPngNativeText() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "Some long image desc"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'ImageDescription', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['ImageDescription'] ); + $this->assertArrayHasKey( '_type', $meta['ImageDescription'] ); + + $this->assertEquals( $expected, $meta['ImageDescription']['x-default'] ); + } + + /** + * tEXt tags must be encoded iso-8859-1 (vs iTXt which are utf-8) + * Make sure non-ascii characters get converted properly + */ + public function testPngNativeTextNonAscii() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + // Note the Copyright symbol here is a utf-8 one + // (aka \xC2\xA9) where in the file its iso-8859-1 + // encoded as just \xA9. + $expected = "© 2010 Bawolff"; + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Copyright', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Copyright'] ); + + $this->assertEquals( $expected, $meta['Copyright']['x-default'] ); + } + + /** + * Test extraction of pHYs tags, which can tell what the + * actual resolution of the image is (aka in dots per meter). + */ + /* + public function testPngPhysTag() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + + $this->assertEquals( '2835/100', $meta['XResolution'] ); + $this->assertEquals( '2835/100', $meta['YResolution'] ); + $this->assertEquals( 3, $meta['ResolutionUnit'] ); // 3 = cm + } + */ + + /** + * Given a normal static PNG, check the animation metadata returned. + */ + public function testStaticPngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 0, $meta['frameCount'] ); + $this->assertEquals( 1, $meta['loopCount'] ); + $this->assertEquals( 0, $meta['duration'] ); + } + + /** + * Given an animated APNG image file + * check it gets animated metadata right. + */ + public function testApngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Animated_PNG_example_bouncing_beach_ball.png' ); + + $this->assertEquals( 20, $meta['frameCount'] ); + // Note loop count of 0 = infinity + $this->assertEquals( 0, $meta['loopCount'] ); + $this->assertEquals( 1.5, $meta['duration'], '', 0.00001 ); + } + + public function testPngBitDepth8() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 8, $meta['bitDepth'] ); + } + + public function testPngBitDepth1() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + '1bit-png.png' ); + $this->assertEquals( 1, $meta['bitDepth'] ); + } + + public function testPngIndexColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 'index-coloured', $meta['colorType'] ); + } + + public function testPngRgbColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-png.png' ); + $this->assertEquals( 'truecolour-alpha', $meta['colorType'] ); + } + + public function testPngRgbNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-na-png.png' ); + $this->assertEquals( 'truecolour', $meta['colorType'] ); + } + + public function testPngGreyscaleColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-png.png' ); + $this->assertEquals( 'greyscale-alpha', $meta['colorType'] ); + } + + public function testPngGreyscaleNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-na-png.png' ); + $this->assertEquals( 'greyscale', $meta['colorType'] ); + } +} diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php new file mode 100644 index 00000000..36872a75 --- /dev/null +++ b/tests/phpunit/includes/media/PNGTest.php @@ -0,0 +1,131 @@ +<?php + +/** + * @group Media + */ +class PNGHandlerTest extends MediaWikiMediaTestCase { + + /** @var PNGHandler */ + protected $handler; + + protected function setUp() { + parent::setUp(); + $this->handler = new PNGHandler(); + } + + /** + * @covers PNGHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . '/README' ); + $this->assertEquals( PNGHandler::BROKEN_FILE, $res ); + } + + /** + * @param string $filename Basename of the file to check + * @param bool $expected Expected result. + * @dataProvider provideIsAnimated + * @covers PNGHandler::isAnimatedImage + */ + public function testIsAnimanted( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsAnimated() { + return array( + array( 'Animated_PNG_example_bouncing_beach_ball.png', true ), + array( '1bit-png.png', false ), + ); + } + + /** + * @param string $filename + * @param int $expected Total image area + * @dataProvider provideGetImageArea + * @covers PNGHandler::getImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetImageArea() { + return array( + array( '1bit-png.png', 2500 ), + array( 'greyscale-png.png', 2500 ), + array( 'Png-native-test.png', 126000 ), + array( 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ), + ); + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of PNGHandler + * @dataProvider provideIsMetadataValid + * @covers PNGHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return array( + array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ), + array( '', PNGHandler::METADATA_BAD ), + array( null, PNGHandler::METADATA_BAD ), + array( 'Something invalid!', PNGHandler::METADATA_BAD ), + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + array( 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', PNGHandler::METADATA_GOOD ), + // @codingStandardsIgnoreEnd + ); + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers PNGHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); +// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + $this->assertEquals( ( $expected ), ( $actual ) ); + } + + public static function provideGetMetadata() { + return array( + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + array( 'rgb-na-png.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' ), + array( 'xmp.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' ), + // @codingStandardsIgnoreEnd + ); + } + + /** + * @param string $filename + * @param array $expected Expected standard metadata + * @dataProvider provideGetIndependentMetaArray + * @covers PNGHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getCommonMetaArray( $file ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetIndependentMetaArray() { + return array( + array( 'rgb-na-png.png', array() ), + array( 'xmp.png', + array( + 'SerialNumber' => '123456789', + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php new file mode 100644 index 00000000..ab33d1c2 --- /dev/null +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -0,0 +1,160 @@ +<?php + +/** + * @group Media + * @covers SVGMetadataExtractor + */ +class SVGMetadataExtractorTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + AutoLoader::loadClass( 'SVGMetadataExtractorTest' ); + } + + /** + * @dataProvider provideSvgFiles + */ + public function testGetMetadata( $infile, $expected ) { + $this->assertMetadata( $infile, $expected ); + } + + /** + * @dataProvider provideSvgFilesWithXMLMetadata + */ + public function testGetXMLMetadata( $infile, $expected ) { + $r = new XMLReader(); + if ( !method_exists( $r, 'readInnerXML' ) ) { + $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' ); + + return; + } + $this->assertMetadata( $infile, $expected ); + } + + function assertMetadata( $infile, $expected ) { + try { + $data = SVGMetadataExtractor::getMetadata( $infile ); + $this->assertEquals( $expected, $data, 'SVG metadata extraction test' ); + } catch ( MWException $e ) { + if ( $expected === false ) { + $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' ); + } else { + throw $e; + } + } + } + + public static function provideSvgFiles() { + $base = __DIR__ . '/../../data/media'; + + return array( + array( + "$base/Wikimedia-logo.svg", + array( + 'width' => 1024, + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', + 'translations' => array(), + ) + ), + array( + "$base/QA_icon.svg", + array( + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', + 'translations' => array(), + ) + ), + array( + "$base/Gtk-media-play-ltr.svg", + array( + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', + 'translations' => array(), + ) + ), + array( + "$base/Toll_Texas_1.svg", + // This file triggered bug 31719, needs entity expansion in the xmlns checks + array( + 'width' => 385, + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', + 'translations' => array(), + ) + ), + array( + "$base/Tux.svg", + array( + 'width' => 512, + 'height' => 594, + 'originalWidth' => '100%', + 'originalHeight' => '100%', + 'title' => 'Tux', + 'translations' => array(), + 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ) + ), + array( + "$base/Speech_bubbles.svg", + array( + 'width' => 627, + 'height' => 461, + 'originalWidth' => '17.7cm', + 'originalHeight' => '13cm', + 'translations' => array( + 'de' => SVGReader::LANG_FULL_MATCH, + 'fr' => SVGReader::LANG_FULL_MATCH, + 'nl' => SVGReader::LANG_FULL_MATCH, + 'tlh-ca' => SVGReader::LANG_FULL_MATCH, + 'tlh' => SVGReader::LANG_PREFIX_MATCH + ), + ) + ), + array( + "$base/Soccer_ball_animated.svg", + array( + 'width' => 150, + 'height' => 150, + 'originalWidth' => '150', + 'originalHeight' => '150', + 'animated' => true, + 'translations' => array() + ), + ), + ); + } + + public static function provideSvgFilesWithXMLMetadata() { + $base = __DIR__ . '/../../data/media'; + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $metadata = '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about=""> + <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format> + <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </ns4:Work> + </rdf:RDF>'; + // @codingStandardsIgnoreEnd + + $metadata = str_replace( "\r", '', $metadata ); // Windows compat + return array( + array( + "$base/US_states_by_total_state_tax_revenue.svg", + array( + 'height' => 593, + 'metadata' => $metadata, + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', + 'translations' => array(), + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/SVGTest.php b/tests/phpunit/includes/media/SVGTest.php new file mode 100644 index 00000000..8f7a0d69 --- /dev/null +++ b/tests/phpunit/includes/media/SVGTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @group Media + */ +class SvgTest extends MediaWikiMediaTestCase { + + protected function setUp() { + parent::setUp(); + + $this->filePath = __DIR__ . '/../../data/media/'; + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->handler = new SvgHandler; + } + + /** + * @param string $filename + * @param array $expected The expected independent metadata + * @dataProvider providerGetIndependentMetaArray + * @covers SvgHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/svg+xml' ); + $res = $this->handler->getCommonMetaArray( $file ); + + $this->assertEquals( $res, $expected ); + } + + public static 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() ) + ); + } +} diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php new file mode 100644 index 00000000..d1148202 --- /dev/null +++ b/tests/phpunit/includes/media/TiffTest.php @@ -0,0 +1,45 @@ +<?php + +/** + * @group Media + */ +class TiffTest extends MediaWikiTestCase { + + /** @var TiffHandler */ + protected $handler; + /** @var string */ + protected $filePath; + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->setMwGlobals( 'wgShowEXIF', true ); + + $this->filePath = __DIR__ . '/../../data/media/'; + $this->handler = new TiffHandler; + } + + /** + * @covers TiffHandler::getMetadata + */ + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + + /** + * @covers TiffHandler::getMetadata + */ + public function testTiffMetadataExtraction() { + $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' ); + + // @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong + $expected = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + // @codingStandardsIgnoreEnd + + // Re-unserialize in case there are subtle differences between how versions + // of php serialize stuff. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/tests/phpunit/includes/media/XCFTest.php b/tests/phpunit/includes/media/XCFTest.php new file mode 100644 index 00000000..5b2de151 --- /dev/null +++ b/tests/phpunit/includes/media/XCFTest.php @@ -0,0 +1,78 @@ +<?php + +/** + * @group Media + */ +class XCFHandlerTest extends MediaWikiMediaTestCase { + + /** @var XCFHandler */ + protected $handler; + + protected function setUp() { + parent::setUp(); + $this->handler = new XCFHandler(); + } + + + /** + * @param string $filename + * @param int $expectedWidth Width + * @param int $expectedHeight Height + * @dataProvider provideGetImageSize + * @covers XCFHandler::getImageSize + */ + public function testGetImageSize( $filename, $expectedWidth, $expectedHeight ) { + $file = $this->dataFile( $filename, 'image/x-xcf' ); + $actual = $this->handler->getImageSize( $file, $file->getLocalRefPath() ); + $this->assertEquals( $expectedWidth, $actual[0] ); + $this->assertEquals( $expectedHeight, $actual[1] ); + } + + public static function provideGetImageSize() { + return array( + array( '80x60-2layers.xcf', 80, 60 ), + array( '80x60-RGB.xcf', 80, 60 ), + array( '80x60-Greyscale.xcf', 80, 60 ), + ); + } + + /** + * @param string $metadata Serialized metadata + * @param int $expected One of the class constants of XCFHandler + * @dataProvider provideIsMetadataValid + * @covers XCFHandler::isMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideIsMetadataValid() { + return array( + array( '', XCFHandler::METADATA_BAD ), + array( serialize( array( 'error' => true ) ), XCFHandler::METADATA_GOOD ), + array( false, XCFHandler::METADATA_BAD ), + array( serialize( array( 'colorType' => 'greyscale-alpha' ) ), XCFHandler::METADATA_GOOD ), + ); + } + + /** + * @param string $filename + * @param string $expected Serialized array + * @dataProvider provideGetMetadata + * @covers XCFHandler::getMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actual = $this->handler->getMetadata( $file, "$this->filePath/$filename" ); + $this->assertEquals( $expected, $actual ); + } + + public static function provideGetMetadata() { + return array( + array( '80x60-2layers.xcf', 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' ), + array( '80x60-RGB.xcf', 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' ), + array( '80x60-Greyscale.xcf', 'a:1:{s:9:"colorType";s:15:"greyscale-alpha";}' ), + ); + } +} diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php new file mode 100644 index 00000000..6758e94c --- /dev/null +++ b/tests/phpunit/includes/media/XMPTest.php @@ -0,0 +1,223 @@ +<?php + +/** + * @group Media + * @covers XMPReader + */ +class XMPTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); # Requires libxml to do XMP parsing + } + + /** + * Put XMP in, compare what comes out... + * + * @param string $xmp The actual xml data. + * @param array $expected Expected result of parsing the xmp. + * @param string $info Short sentence on what's being tested. + * + * @throws Exception + * @dataProvider provideXMPParse + * + * @covers XMPReader::parse + */ + public function testXMPParse( $xmp, $expected, $info ) { + if ( !is_string( $xmp ) || !is_array( $expected ) ) { + throw new Exception( "Invalid data provided to " . __METHOD__ ); + } + $reader = new XMPReader; + $reader->parse( $xmp ); + $this->assertEquals( $expected, $reader->getResults(), $info, 0.0000000001 ); + } + + public static function provideXMPParse() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $data = array(); + + // $xmpFiles format: array of arrays with first arg file base name, + // with the actual file having .xmp on the end for the xmp + // and .result.php on the end for a php file containing the result + // array. Second argument is some info on what's being tested. + $xmpFiles = array( + array( '1', 'parseType=Resource test' ), + array( '2', 'Structure with mixed attribute and element props' ), + array( '3', 'Extra qualifiers (that should be ignored)' ), + array( '3-invalid', 'Test ignoring qualifiers that look like normal props' ), + array( '4', 'Flash as qualifier' ), + array( '5', 'Flash as qualifier 2' ), + array( '6', 'Multiple rdf:Description' ), + array( '7', 'Generic test of several property types' ), + array( 'flash', 'Test of Flash property' ), + array( 'invalid-child-not-struct', 'Test child props not in struct or ignored' ), + array( 'no-recognized-props', 'Test namespace and no recognized props' ), + array( 'no-namespace', 'Test non-namespaced attributes are ignored' ), + array( 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ), + array( 'utf16BE', 'UTF-16BE encoding' ), + array( 'utf16LE', 'UTF-16LE encoding' ), + array( 'utf32BE', 'UTF-32BE encoding' ), + array( 'utf32LE', 'UTF-32LE encoding' ), + array( 'xmpExt', 'Extended XMP missing second part' ), + array( 'gps', 'Handling of exif GPS parameters in XMP' ), + ); + + $xmpFiles[] = array( 'doctype-included', 'XMP includes doctype' ); + + foreach ( $xmpFiles as $file ) { + $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' ); + // I'm not sure if this is the best way to handle getting the + // result array, but it seems kind of big to put directly in the test + // file. + $result = null; + include $xmpPath . $file[0] . '.result.php'; + $data[] = array( $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] ); + } + + return $data; + } + + /** Test ExtendedXMP block support. (Used when the XMP has to be split + * over multiple jpeg segments, due to 64k size limit on jpeg segments. + * + * @todo This is based on what the standard says. Need to find a real + * world example file to double check the support for this is right. + * + * @covers XMPReader::parseExtended + */ + public function testExtendedXMP() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + 'FNumber' => '2/10', + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * This test has an extended XMP block with a wrong guid (md5sum) + * and thus should only return the StandardXMP, not the ExtendedXMP. + * + * @covers XMPReader::parseExtended + */ + public function testExtendedXMPWithWrongGUID() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit. + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * Have a high offset to simulate a missing packet, + * which should cause it to ignore the ExtendedXMP packet. + * + * @covers XMPReader::parseExtended + */ + public function testExtendedXMPMissingPacket() { + $xmpPath = __DIR__ . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 2048 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * Test for multi-section, hostile XML + * @covers checkParseSafety + */ + public function testCheckParseSafety() { + + // Test for detection + $xmpPath = __DIR__ . '/../../data/xmp/'; + $file = fopen( $xmpPath . 'doctype-included.xmp', 'rb' ); + $valid = false; + $reader = new XMPReader(); + do { + $chunk = fread( $file, 10 ); + $valid = $reader->parse( $chunk, feof( $file ) ); + } while ( !feof( $file ) ); + $this->assertFalse( $valid, 'Check that doctype is detected in fragmented XML' ); + $this->assertEquals( + array(), + $reader->getResults(), + 'Check that doctype is detected in fragmented XML' + ); + fclose( $file ); + unset( $reader ); + + // Test for false positives + $file = fopen( $xmpPath . 'doctype-not-included.xmp', 'rb' ); + $valid = false; + $reader = new XMPReader(); + do { + $chunk = fread( $file, 10 ); + $valid = $reader->parse( $chunk, feof( $file ) ); + } while ( !feof( $file ) ); + $this->assertTrue( + $valid, + 'Check for false-positive detecting doctype in fragmented XML' + ); + $this->assertEquals( + array( + 'xmp-exif' => array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) + ), + $reader->getResults(), + 'Check that doctype is detected in fragmented XML' + ); + } +} diff --git a/tests/phpunit/includes/media/XMPValidateTest.php b/tests/phpunit/includes/media/XMPValidateTest.php new file mode 100644 index 00000000..ebec8f6c --- /dev/null +++ b/tests/phpunit/includes/media/XMPValidateTest.php @@ -0,0 +1,50 @@ +<?php + +/** + * @group Media + */ +class XMPValidateTest extends MediaWikiTestCase { + + /** + * @dataProvider provideDates + * @covers XMPValidate::validateDate + */ + public function testValidateDate( $value, $expected ) { + // The method should modify $value. + XMPValidate::validateDate( array(), $value, true ); + $this->assertEquals( $expected, $value ); + } + + public static function provideDates() { + /* For reference valid date formats are: + * YYYY + * YYYY-MM + * YYYY-MM-DD + * YYYY-MM-DDThh:mmTZD + * YYYY-MM-DDThh:mm:ssTZD + * YYYY-MM-DDThh:mm:ss.sTZD + * (Time zone is optional) + */ + return array( + array( '1992', '1992' ), + array( '1992-04', '1992:04' ), + array( '1992-02-01', '1992:02:01' ), + array( '2011-09-29', '2011:09:29' ), + array( '1982-12-15T20:12', '1982:12:15 20:12' ), + array( '1982-12-15T20:12Z', '1982:12:15 20:12' ), + array( '1982-12-15T20:12+02:30', '1982:12:15 22:42' ), + array( '1982-12-15T01:12-02:30', '1982:12:14 22:42' ), + array( '1982-12-15T20:12:11', '1982:12:15 20:12:11' ), + array( '1982-12-15T20:12:11Z', '1982:12:15 20:12:11' ), + array( '1982-12-15T20:12:11+01:10', '1982:12:15 21:22:11' ), + array( '2045-12-15T20:12:11', '2045:12:15 20:12:11' ), + array( '1867-06-01T15:00:00', '1867:06:01 15:00:00' ), + /* some invalid ones */ + array( '2001--12', null ), + array( '2001-5-12', null ), + array( '2001-5-12TZ', null ), + array( '2001-05-12T15', null ), + array( '2001-12T15:13', null ), + ); + } +} |