diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/media')
23 files changed, 2762 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/www/wiki/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php new file mode 100644 index 00000000..a70c0054 --- /dev/null +++ b/www/wiki/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 = [ + '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( [ 'foo' => 'xmp' ], 'xmp-general' ); + $merger->addMetadata( [ 'bar' => 'xmp' ], 'xmp-general' ); + $merger->addMetadata( [ 'baz' => 'xmp' ], 'xmp-general' ); + $merger->addMetadata( [ 'fred' => 'xmp' ], 'xmp-general' ); + $merger->addMetadata( [ 'foo' => 'iptc (hash)' ], 'iptc-good-hash' ); + $merger->addMetadata( [ 'bar' => 'iptc (bad hash)' ], 'iptc-bad-hash' ); + $merger->addMetadata( [ 'baz' => 'iptc (bad hash)' ], 'iptc-bad-hash' ); + $merger->addMetadata( [ 'fred' => 'iptc (no hash)' ], 'iptc-no-hash' ); + $merger->addMetadata( [ 'baz' => 'exif' ], 'exif' ); + + $actual = $merger->getMetadataArray(); + $expected = [ + '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 = [ + 'frameCount' => 0, + 'loopCount' => 1, + 'duration' => 0, + 'bitDepth' => 1, + 'colorType' => 'index-coloured', + 'metadata' => [ + '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/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php b/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php new file mode 100644 index 00000000..fb96f7db --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/BitmapScalingTest.php @@ -0,0 +1,151 @@ +<?php + +/** + * @group Media + */ +class BitmapScalingTest extends MediaWikiTestCase { + + protected function setUp() { + parent::setUp(); + + $this->setMwGlobals( [ + '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 [ + /* Regular resize operations */ + [ + [ 1024, 768 ], + [ + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 512 ], + 'Resizing with width set', + ], + [ + [ 1024, 768 ], + [ + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 512, 'height' => 768 ], + 'Resizing with height set too high', + ], + [ + [ 1024, 768 ], + [ + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 1024, 'height' => 384 ], + 'Resizing with height set', + ], + + /* Very tall images */ + [ + [ 1000, 100 ], + [ + 'width' => 5, 'height' => 1, + 'physicalWidth' => 5, 'physicalHeight' => 1, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 5 ], + 'Very wide image', + ], + + [ + [ 100, 1000 ], + [ + 'width' => 1, 'height' => 10, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 1 ], + 'Very high image', + ], + [ + [ 100, 1000 ], + [ + 'width' => 1, 'height' => 5, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 10, 'height' => 5 ], + 'Very high image with height set', + ], + /* Max image area */ + [ + [ 4000, 4000 ], + [ + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 5000 ], + 'Bigger than max image size but doesn\'t need scaling', + ], + /* Max interlace image area */ + [ + [ 4000, 4000 ], + [ + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, 'interlace' => false, + ], + [ 'width' => 5000, 'interlace' => true ], + 'Interlace bigger than max interlace area', + ], + ]; + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigImage() { + $file = new FakeDimensionFile( [ 4000, 4000 ] ); + $handler = new BitmapHandler; + $params = [ 'width' => '3700' ]; // Still bigger than max size. + $this->assertEquals( TransformTooBigImageAreaError::class, + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::doTransform + */ + public function testTooBigMustRenderImage() { + $file = new FakeDimensionFile( [ 4000, 4000 ] ); + $file->mustRender = true; + $handler = new BitmapHandler; + $params = [ 'width' => '5000' ]; // Still bigger than max size. + $this->assertEquals( TransformTooBigImageAreaError::class, + get_class( $handler->doTransform( $file, 'dummy path', '', $params ) ) ); + } + + /** + * @covers BitmapHandler::getImageArea + */ + public function testImageArea() { + $file = new FakeDimensionFile( [ 7, 9 ] ); + $handler = new BitmapHandler; + $this->assertEquals( 63, $handler->getImageArea( $file ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/DjVuTest.php b/www/wiki/tests/phpunit/includes/media/DjVuTest.php new file mode 100644 index 00000000..dbc0d2fb --- /dev/null +++ b/www/wiki/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( + [ 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( + [ 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/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php b/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php new file mode 100644 index 00000000..eb02e7ed --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/ExifBitmapTest.php @@ -0,0 +1,142 @@ +<?php + +/** + * @group Media + */ +class ExifBitmapTest extends MediaWikiMediaTestCase { + + /** + * @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() { + // phpcs:ignore Generic.Files.LineLength + $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;}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + + /** + * @covers ExifBitmapHandler::isMetadataValid + */ + public function testIsOldGood() { + // phpcs:ignore Generic.Files.LineLength + $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;}'; + $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() { + // phpcs:ignore Generic.Files.LineLength + $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";}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + /** + * @covers ExifBitmapHandler::convertMetadataVersion + */ + public function testConvertMetadataLatest() { + $metadata = [ + 'foo' => [ '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 = [ + 'foo' => [ 'First', 'Second', '_type' => 'ol' ], + 'bar' => [ 'First', 'Second', '_type' => 'ul' ], + 'baz' => [ 'First', 'Second' ], + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 2, + ]; + $expected = [ + '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 = [ + 'Software' => [ [ 'GIMP', '1.1' ] ], + 'MEDIAWIKI_EXIF_VERSION' => 2, + ]; + $expected = [ + '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 = [ + 'Software' => [ "GIMP 1.2", "vim" ], + 'MEDIAWIKI_EXIF_VERSION' => 2, + ]; + $expected = [ + 'Software' => "\n*GIMP 1.2\n*vim", + 'MEDIAWIKI_EXIF_VERSION' => 1, + ]; + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php b/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php new file mode 100644 index 00000000..fff101f3 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/ExifRotationTest.php @@ -0,0 +1,283 @@ +<?php +/** + * Tests related to auto rotation. + * + * @group Media + * @group medium + * + * @covers BitmapHandler + */ +class ExifRotationTest extends MediaWikiMediaTestCase { + + /** @var BitmapHandler */ + private $handler; + + protected function setUp() { + parent::setUp(); + $this->checkPHPExtension( 'exif' ); + + $this->handler = new BitmapHandler(); + + $this->setMwGlobals( [ + '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 = [ + 'width' => $matches[1], + ]; + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = [ + '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 [ + [ + 'landscape-plain.jpg', + 'image/jpeg', + [ + 'width' => 1024, + 'height' => 768, + ], + [ + '800x600px' => [ 800, 600 ], + '9999x800px' => [ 1067, 800 ], + '800px' => [ 800, 600 ], + '600px' => [ 600, 450 ], + ] + ], + [ + 'portrait-rotated.jpg', + 'image/jpeg', + [ + 'width' => 768, // as rotated + 'height' => 1024, // as rotated + ], + [ + '800x600px' => [ 450, 600 ], + '9999x800px' => [ 600, 800 ], + '800px' => [ 800, 1067 ], + '600px' => [ 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 = [ + 'width' => $matches[1], + ]; + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = [ + '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 [ + [ + 'landscape-plain.jpg', + 'image/jpeg', + [ + 'width' => 1024, + 'height' => 768, + ], + [ + '800x600px' => [ 800, 600 ], + '9999x800px' => [ 1067, 800 ], + '800px' => [ 800, 600 ], + '600px' => [ 600, 450 ], + ] + ], + [ + 'portrait-rotated.jpg', + 'image/jpeg', + [ + 'width' => 1024, // since not rotated + 'height' => 768, // since not rotated + ], + [ + '800x600px' => [ 800, 600 ], + '9999x800px' => [ 1067, 800 ], + '800px' => [ 800, 600 ], + '600px' => [ 600, 450 ], + ] + ] + ]; + } + + const TEST_WIDTH = 100; + const TEST_HEIGHT = 200; + + /** + * @dataProvider provideBitmapExtractPreRotationDimensions + */ + public function testBitmapExtractPreRotationDimensions( $rotation, $expected ) { + $result = $this->handler->extractPreRotationDimensions( [ + 'physicalWidth' => self::TEST_WIDTH, + 'physicalHeight' => self::TEST_HEIGHT, + ], $rotation ); + $this->assertEquals( $expected, $result ); + } + + public static function provideBitmapExtractPreRotationDimensions() { + return [ + [ + 0, + [ self::TEST_WIDTH, self::TEST_HEIGHT ] + ], + [ + 90, + [ self::TEST_HEIGHT, self::TEST_WIDTH ] + ], + [ + 180, + [ self::TEST_WIDTH, self::TEST_HEIGHT ] + ], + [ + 270, + [ self::TEST_HEIGHT, self::TEST_WIDTH ] + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/ExifTest.php b/www/wiki/tests/phpunit/includes/media/ExifTest.php new file mode 100644 index 00000000..876e4617 --- /dev/null +++ b/www/wiki/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 = [ + '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 = [ + 'UserComment' => 'test⁔comment', + ]; + $this->assertEquals( $expected, $data ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php b/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php new file mode 100644 index 00000000..5e3a3cba --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/FakeDimensionFile.php @@ -0,0 +1,35 @@ +<?php + +class FakeDimensionFile extends File { + public $mustRender = false; + public $mime; + public $dimensions; + + public function __construct( $dimensions, $mime = 'unknown/unknown' ) { + parent::__construct( Title::makeTitle( NS_FILE, 'Test' ), + new NullRepo( null ) ); + + $this->dimensions = $dimensions; + $this->mime = $mime; + } + + 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 ''; + } + + public function getMimeType() { + return $this->mime; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php b/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php new file mode 100644 index 00000000..0987bd0a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/FormatMetadataTest.php @@ -0,0 +1,144 @@ +<?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 (T31471)' ); + } + + /** + * @param mixed $input + * @param mixed $output + * @dataProvider provideResolveMultivalueValue + * @covers FormatMetadata::resolveMultivalueValue + */ + public function testResolveMultivalueValue( $input, $output ) { + $formatMetadata = new FormatMetadata(); + $class = new ReflectionClass( FormatMetadata::class ); + $method = $class->getMethod( 'resolveMultivalueValue' ); + $method->setAccessible( true ); + $actualInput = $method->invoke( $formatMetadata, $input ); + $this->assertEquals( $output, $actualInput ); + } + + public function provideResolveMultivalueValue() { + return [ + 'nonArray' => [ + 'foo', + 'foo' + ], + 'multiValue' => [ + [ 'first', 'second', 'third', '_type' => 'ol' ], + 'first' + ], + 'noType' => [ + [ 'first', 'second', 'third' ], + 'first' + ], + 'typeFirst' => [ + [ '_type' => 'ol', 'first', 'second', 'third' ], + 'first' + ], + 'multilang' => [ + [ + 'en' => 'first', + 'de' => 'Erste', + '_type' => 'lang' + ], + [ + 'en' => 'first', + 'de' => 'Erste', + '_type' => 'lang' + ], + ], + 'multilang-multivalue' => [ + [ + 'en' => [ 'first', 'second' ], + 'de' => [ 'Erste', 'Zweite' ], + '_type' => 'lang' + ], + [ + 'en' => 'first', + 'de' => 'Erste', + '_type' => 'lang' + ], + ], + ]; + } + + /** + * @param mixed $input + * @param mixed $output + * @dataProvider provideGetFormattedData + * @covers FormatMetadata::getFormattedData + */ + public function testGetFormattedData( $input, $output ) { + $this->assertEquals( $output, FormatMetadata::getFormattedData( $input ) ); + } + + public function provideGetFormattedData() { + return [ + [ + [ 'Software' => 'Adobe Photoshop CS6 (Macintosh)' ], + [ 'Software' => 'Adobe Photoshop CS6 (Macintosh)' ], + ], + [ + [ 'Software' => [ 'FotoWare FotoStation' ] ], + [ 'Software' => 'FotoWare FotoStation' ], + ], + [ + [ 'Software' => [ [ 'Capture One PRO', '3.7.7' ] ] ], + [ 'Software' => 'Capture One PRO (Version 3.7.7)' ], + ], + [ + [ 'Software' => [ [ 'FotoWare ColorFactory', '' ] ] ], + [ 'Software' => 'FotoWare ColorFactory (Version )' ], + ], + [ + [ 'Software' => [ 'x-default' => 'paint.net 4.0.12', '_type' => 'lang' ] ], + [ 'Software' => '<ul class="metadata-langlist">'. + '<li class="mw-metadata-lang-default">'. + '<span class="mw-metadata-lang-value">paint.net 4.0.12</span>'. + "</li>\n". + '</ul>' + ], + ], + [ + // https://phabricator.wikimedia.org/T178130 + // WebMHandler.php turns both 'muxingapp' & 'writingapp' to 'Software' + [ 'Software' => [ [ 'Lavf57.25.100' ], [ 'Lavf57.25.100' ] ] ], + [ 'Software' => "<ul><li>Lavf57.25.100</li>\n<li>Lavf57.25.100</li></ul>" ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/GIFMetadataExtractorTest.php new file mode 100644 index 00000000..278b441b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -0,0 +1,110 @@ +<?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 [ + [ + 'nonanimated.gif', + [ + 'comment' => [ 'GIF test file ⁕ Created with GIMP' ], + 'duration' => 0.1, + 'frameCount' => 1, + 'looped' => false, + 'xmp' => '', + ] + ], + [ + 'animated.gif', + [ + 'comment' => [ 'GIF test file . Created with GIMP' ], + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'xmp' => '', + ] + ], + + [ + 'animated-xmp.gif', + [ + 'xmp' => $xmpNugget, + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'comment' => [ 'GIƒ·test·file' ], + ] + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/GIFTest.php b/www/wiki/tests/phpunit/includes/media/GIFTest.php new file mode 100644 index 00000000..4dd7443e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/GIFTest.php @@ -0,0 +1,172 @@ +<?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 [ + [ 'animated.gif', true ], + [ '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 [ + [ 'animated.gif', 5400 ], + [ '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() { + // phpcs:disable Generic.Files.LineLength + return [ + [ GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ], + [ '', GIFHandler::METADATA_BAD ], + [ null, GIFHandler::METADATA_BAD ], + [ 'Something invalid!', GIFHandler::METADATA_BAD ], + [ + '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 + ], + ]; + // phpcs:enable + } + + /** + * @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() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + '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;}}' + ], + [ + '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;}}' + ], + ]; + // phpcs:enable + } + + /** + * @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 [ + [ 'nonanimated.gif', [ + 'GIFFileComment' => [ + 'GIF test file ⁕ Created with GIMP', + ], + ] ], + [ 'animated-xmp.gif', + [ + 'Artist' => 'Bawolff', + 'ImageDescription' => [ + 'x-default' => 'A file to test GIF', + '_type' => 'lang', + ], + 'SublocationDest' => 'The interwebs', + 'GIFFileComment' => + [ + 'GIƒ·test·file', + ], + ] + ], + ]; + } + + /** + * @param string $filename + * @param float $expectedLength + * @dataProvider provideGetLength + * @covers GIFHandler::getLength + */ + public function testGetLength( $filename, $expectedLength ) { + $file = $this->dataFile( $filename, 'image/gif' ); + $actualLength = $file->getLength(); + $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); + } + + public function provideGetLength() { + return [ + [ 'animated.gif', 2.4 ], + [ 'animated-xmp.gif', 2.4 ], + [ 'nonanimated', 0.0 ], + [ 'Bishzilla_blink.gif', 1.4 ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/IPTCTest.php b/www/wiki/tests/phpunit/includes/media/IPTCTest.php new file mode 100644 index 00000000..4b3ba075 --- /dev/null +++ b/www/wiki/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( [ '¼' ], $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( [ 'ÃÃø' ], $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( [ 'ø' ], $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( [ '¼' ], $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( [ '¼', '¼½' ], $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( [ '¼' ], $res['Keywords'] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php new file mode 100644 index 00000000..c943cef9 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -0,0 +1,128 @@ +<?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( [ 'UTF-8 JPEG Comment — ¼' ], $res['COM'] ); + } + + public static function provideUtf8Comment() { + return [ + [ 'jpeg-comment-utf.jpg' ], + [ 'jpeg-padding-even.jpg' ], + [ '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( [ '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( [ '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'] ); + } + + public function testInfiniteRead() { + // test file truncated right after a segment, which previously + // caused an infinite loop looking for the next segment byte. + // Should get past infinite loop and throw in wfUnpack() + $this->setExpectedException( 'MWException' ); + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop1.jpg' ); + } + + public function testInfiniteRead2() { + // test file truncated after a segment's marker and size, which + // would cause a seek past end of file. Seek past end of file + // doesn't actually fail, but prevents further reading and was + // devolving into the previous case (testInfiniteRead). + $this->setExpectedException( 'MWException' ); + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-segment-loop2.jpg' ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/JpegPixelFormatTest.php b/www/wiki/tests/phpunit/includes/media/JpegPixelFormatTest.php new file mode 100644 index 00000000..6815a62b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/JpegPixelFormatTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Tests related to JPEG chroma subsampling via $wgJpegPixelFormat setting. + * + * @group Media + * @group medium + * + * @todo covers tags + */ +class JpegPixelFormatTest extends MediaWikiMediaTestCase { + + protected function setUp() { + parent::setUp(); + } + + /** + * Mark this test as creating thumbnail files. + */ + protected function createsThumbnails() { + return true; + } + + /** + * + * @dataProvider providePixelFormats + */ + public function testPixelFormatRendering( $sourceFile, $pixelFormat, $samplingFactor ) { + global $wgUseImageMagick, $wgUseImageResize; + if ( !$wgUseImageMagick ) { + $this->markTestSkipped( "This test is only applicable when using ImageMagick thumbnailing" ); + } + if ( !$wgUseImageResize ) { + $this->markTestSkipped( "This test is only applicable when using thumbnailing" ); + } + + $fmtStr = var_export( $pixelFormat, true ); + $this->setMwGlobals( 'wgJpegPixelFormat', $pixelFormat ); + + $file = $this->dataFile( $sourceFile, 'image/jpeg' ); + + $params = [ + 'width' => 320, + ]; + $thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE ); + $this->assertTrue( !$thumb->isError(), "created JPEG thumbnail for pixel format $fmtStr" ); + + $path = $thumb->getLocalCopyPath(); + $this->assertTrue( is_string( $path ), "path returned for JPEG thumbnail for $fmtStr" ); + + $cmd = [ + 'identify', + '-format', + '%[jpeg:sampling-factor]', + $path + ]; + $retval = null; + $output = wfShellExec( $cmd, $retval ); + $this->assertTrue( $retval === 0, "ImageMagick's identify command should return success" ); + + $expected = $samplingFactor; + $actual = trim( $output ); + $this->assertEquals( + $expected, + trim( $output ), + "IM identify expects JPEG chroma subsampling \"$expected\" for $fmtStr" + ); + } + + public static function providePixelFormats() { + return [ + // From 4:4:4 source file + [ + 'yuv444.jpg', + false, + '1x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv444', + '1x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv422', + '2x1,1x1,1x1' + ], + [ + 'yuv444.jpg', + 'yuv420', + '2x2,1x1,1x1' + ], + // From 4:2:0 source file + [ + 'yuv420.jpg', + false, + '2x2,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv444', + '1x1,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv422', + '2x1,1x1,1x1' + ], + [ + 'yuv420.jpg', + 'yuv420', + '2x2,1x1,1x1' + ] + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/JpegTest.php b/www/wiki/tests/phpunit/includes/media/JpegTest.php new file mode 100644 index 00000000..13de7ff9 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/JpegTest.php @@ -0,0 +1,122 @@ +<?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' ); + // phpcs:ignore Generic.Files.LineLength + $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 ) ); + } + + /** + * @covers JpegHandler::getCommonMetaArray + */ + public function testGetIndependentMetaArray() { + $file = $this->dataFile( 'test.jpg', 'image/jpeg' ); + $res = $this->handler->getCommonMetaArray( $file ); + $expected = [ + 'ImageDescription' => 'Test file', + 'XResolution' => '72/1', + 'YResolution' => '72/1', + 'ResolutionUnit' => 2, + 'YCbCrPositioning' => 1, + 'JPEGFileComment' => [ + 'Created with GIMP', + ], + ]; + + $this->assertEquals( $res, $expected ); + } + + /** + * @dataProvider provideSwappingICCProfile + * @covers JpegHandler::swapICCProfile + */ + public function testSwappingICCProfile( + $sourceFilename, $controlFilename, $newProfileFilename, $oldProfileName + ) { + global $wgExiftool; + + if ( !$wgExiftool || !is_file( $wgExiftool ) ) { + $this->markTestSkipped( "Exiftool not installed, cannot test ICC profile swapping" ); + } + + $this->setMwGlobals( 'wgUseTinyRGBForJPGThumbnails', true ); + + $sourceFilepath = $this->filePath . $sourceFilename; + $controlFilepath = $this->filePath . $controlFilename; + $profileFilepath = $this->filePath . $newProfileFilename; + $filepath = $this->getNewTempFile(); + + copy( $sourceFilepath, $filepath ); + + $file = $this->dataFile( $sourceFilename, 'image/jpeg' ); + $this->handler->swapICCProfile( + $filepath, + [ 'sRGB', '-' ], + [ $oldProfileName ], + $profileFilepath + ); + + $this->assertEquals( + sha1( file_get_contents( $filepath ) ), + sha1( file_get_contents( $controlFilepath ) ) + ); + } + + public function provideSwappingICCProfile() { + return [ + // File with sRGB should end up with TinyRGB + [ + 'srgb.jpg', + 'tinyrgb.jpg', + 'tinyrgb.icc', + 'sRGB IEC61966-2.1' + ], + // File with TinyRGB should be left unchanged + [ + 'tinyrgb.jpg', + 'tinyrgb.jpg', + 'tinyrgb.icc', + 'sRGB IEC61966-2.1' + ], + // File without profile should end up with TinyRGB + [ + 'missingprofile.jpg', + 'tinyrgb.jpg', + 'tinyrgb.icc', + 'sRGB IEC61966-2.1' + ], + // Non-sRGB file should be left untouched + [ + 'adobergb.jpg', + 'adobergb.jpg', + 'tinyrgb.icc', + 'sRGB IEC61966-2.1' + ] + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/MediaHandlerTest.php b/www/wiki/tests/phpunit/includes/media/MediaHandlerTest.php new file mode 100644 index 00000000..7a052f60 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/MediaHandlerTest.php @@ -0,0 +1,68 @@ +<?php + +/** + * @group Media + */ +class MediaHandlerTest extends MediaWikiTestCase { + + /** + * @covers MediaHandler::fitBoxWidth + * + * @dataProvider provideTestFitBoxWidth + */ + public function testFitBoxWidth( $width, $height, $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: {z$result}x$y2" ); + } + + public static function provideTestFitBoxWidth() { + return array_merge( + static::generateTestFitBoxWidthData( 50, 50, [ + 50 => 50, + 17 => 17, + 18 => 18 ] + ), + static::generateTestFitBoxWidthData( 366, 300, [ + 50 => 61, + 17 => 21, + 18 => 22 ] + ), + static::generateTestFitBoxWidthData( 300, 366, [ + 50 => 41, + 17 => 14, + 18 => 15 ] + ), + static::generateTestFitBoxWidthData( 100, 400, [ + 50 => 12, + 17 => 4, + 18 => 4 ] + ) + ); + } + + /** + * Generate single test cases by combining the dimensions and tests contents + * + * It creates: + * [$width, $height, $max, $expected], + * [$width, $height, $max2, $expected2], ... + * out of parameters: + * $width, $height, { $max => $expected, $max2 => $expected2, ... } + * + * @param int $width + * @param int $height + * @param array $tests associative array of $max => $expected values + * @return array + */ + private static function generateTestFitBoxWidthData( $width, $height, $tests ) { + $result = []; + foreach ( $tests as $max => $expected ) { + $result[] = [ $width, $height, $max, $expected ]; + } + return $result; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php b/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php new file mode 100644 index 00000000..a4e8056a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/MediaWikiMediaTestCase.php @@ -0,0 +1,86 @@ +<?php +/** + * Specificly for testing Media handlers. Sets up a FileRepo backend + */ +abstract class MediaWikiMediaTestCase extends MediaWikiTestCase { + + /** @var FileRepo */ + protected $repo; + /** @var FSFileBackend */ + protected $backend; + /** @var string */ + protected $filePath; + + protected function setUp() { + parent::setUp(); + + $this->filePath = $this->getFilePath(); + $containers = [ '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 repo named "temp". + $containers['temp-thumb'] = $this->getNewTempDirectory(); + } + + $this->backend = new FSFileBackend( [ + 'name' => 'localtesting', + 'wikiId' => wfWikiID(), + 'containerPaths' => $containers, + 'tmpDirectory' => $this->getNewTempDirectory() + ] ); + $this->repo = new FileRepo( $this->getRepoOptions() ); + } + + /** + * @return array Argument for FileRepo constructor + */ + protected function getRepoOptions() { + return [ + '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 = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); + $parts = explode( $name, '.' ); + $type = $magic->guessTypesForExtension( $parts[count( $parts ) - 1] ); + } + return new UnregisteredLocalFile( false, $this->repo, + "mwstore://localtesting/data/$name", $type ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php new file mode 100644 index 00000000..22de9357 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -0,0 +1,137 @@ +<?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'] ); + } + + /** + * 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/www/wiki/tests/phpunit/includes/media/PNGTest.php b/www/wiki/tests/phpunit/includes/media/PNGTest.php new file mode 100644 index 00000000..5a66586e --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/PNGTest.php @@ -0,0 +1,161 @@ +<?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 [ + [ 'Animated_PNG_example_bouncing_beach_ball.png', true ], + [ '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 [ + [ '1bit-png.png', 2500 ], + [ 'greyscale-png.png', 2500 ], + [ 'Png-native-test.png', 126000 ], + [ '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() { + // phpcs:disable Generic.Files.LineLength + return [ + [ PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ], + [ '', PNGHandler::METADATA_BAD ], + [ null, PNGHandler::METADATA_BAD ], + [ 'Something invalid!', PNGHandler::METADATA_BAD ], + [ + '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 + ], + ]; + // phpcs:enable + } + + /** + * @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() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + '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;}}' + ], + [ + '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;}}' + ], + ]; + // phpcs:enable + } + + /** + * @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 [ + [ 'rgb-na-png.png', [] ], + [ 'xmp.png', + [ + 'SerialNumber' => '123456789', + ] + ], + ]; + } + + /** + * @param string $filename + * @param float $expectedLength + * @dataProvider provideGetLength + * @covers PNGHandler::getLength + */ + public function testGetLength( $filename, $expectedLength ) { + $file = $this->dataFile( $filename, 'image/png' ); + $actualLength = $file->getLength(); + $this->assertEquals( $expectedLength, $actualLength, '', 0.00001 ); + } + + public function provideGetLength() { + return [ + [ 'Animated_PNG_example_bouncing_beach_ball.png', 1.5 ], + [ 'Png-native-test.png', 0.0 ], + [ 'greyscale-png.png', 0.0 ], + [ '1bit-png.png', 0.0 ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php new file mode 100644 index 00000000..6fbb4740 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -0,0 +1,155 @@ +<?php + +/** + * @group Media + * @covers SVGMetadataExtractor + */ +class SVGMetadataExtractorTest extends MediaWikiTestCase { + + /** + * @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 [ + [ + "$base/Wikimedia-logo.svg", + [ + 'width' => 1024, + 'height' => 1024, + 'originalWidth' => '1024', + 'originalHeight' => '1024', + 'translations' => [], + ] + ], + [ + "$base/QA_icon.svg", + [ + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60', + 'originalHeight' => '60', + 'translations' => [], + ] + ], + [ + "$base/Gtk-media-play-ltr.svg", + [ + 'width' => 60, + 'height' => 60, + 'originalWidth' => '60.0000000', + 'originalHeight' => '60.0000000', + 'translations' => [], + ] + ], + [ + "$base/Toll_Texas_1.svg", + // This file triggered T33719, needs entity expansion in the xmlns checks + [ + 'width' => 385, + 'height' => 385, + 'originalWidth' => '385', + 'originalHeight' => '385.0004883', + 'translations' => [], + ] + ], + [ + "$base/Tux.svg", + [ + 'width' => 512, + 'height' => 594, + 'originalWidth' => '100%', + 'originalHeight' => '100%', + 'title' => 'Tux', + 'translations' => [], + 'description' => 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ] + ], + [ + "$base/Speech_bubbles.svg", + [ + 'width' => 627, + 'height' => 461, + 'originalWidth' => '17.7cm', + 'originalHeight' => '13cm', + 'translations' => [ + '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 + ], + ] + ], + [ + "$base/Soccer_ball_animated.svg", + [ + 'width' => 150, + 'height' => 150, + 'originalWidth' => '150', + 'originalHeight' => '150', + 'animated' => true, + 'translations' => [] + ], + ], + ]; + } + + public static function provideSvgFilesWithXMLMetadata() { + $base = __DIR__ . '/../../data/media'; + // phpcs:disable Generic.Files.LineLength + $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>'; + // phpcs:enable + + $metadata = str_replace( "\r", '', $metadata ); // Windows compat + return [ + [ + "$base/US_states_by_total_state_tax_revenue.svg", + [ + 'height' => 593, + 'metadata' => $metadata, + 'width' => 959, + 'originalWidth' => '958.69', + 'originalHeight' => '592.78998', + 'translations' => [], + ] + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/SVGTest.php b/www/wiki/tests/phpunit/includes/media/SVGTest.php new file mode 100644 index 00000000..b68dd0ee --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/SVGTest.php @@ -0,0 +1,113 @@ +<?php + +/** + * @group Media + */ +class SVGTest extends MediaWikiMediaTestCase { + + /** + * @var SvgHandler + */ + private $handler; + + 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 [ + [ 'Tux.svg', [ + 'ObjectName' => 'Tux', + 'ImageDescription' => + 'For more information see: http://commons.wikimedia.org/wiki/Image:Tux.svg', + ] ], + [ 'Wikimedia-logo.svg', [] ] + ]; + } + + /** + * @param string $userPreferredLanguage + * @param array $svgLanguages + * @param string $expectedMatch + * @dataProvider providerGetMatchedLanguage + * @covers SvgHandler::getMatchedLanguage + */ + public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) { + $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages ); + $this->assertEquals( $expectedMatch, $match ); + } + + public function providerGetMatchedLanguage() { + return [ + 'no match' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ], + 'expectedMatch' => null, + ], + 'no subtags' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ], + 'expectedMatch' => 'en', + ], + 'user no subtags, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-GB', + ], + 'user no subtags, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Cyrl-BA', + ], + 'user 1 subtag, svg no subtags' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de', 'en', 'en', 'fr' ], + 'expectedMatch' => null, + ], + 'user 1 subtag, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-US', + ], + 'user 1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg <=1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ], + 'expectedMatch' => null, + ], + 'ensure case-insensitive' => [ + 'userPreferredLanguage' => 'sr-latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/media/TiffTest.php b/www/wiki/tests/phpunit/includes/media/TiffTest.php new file mode 100644 index 00000000..8a69ec5b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/TiffTest.php @@ -0,0 +1,44 @@ +<?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' ); + + // phpcs:ignore Generic.Files.LineLength + $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;}'; + + // Re-unserialize in case there are subtle differences between how versions + // of php serialize stuff. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/media/WebPTest.php b/www/wiki/tests/phpunit/includes/media/WebPTest.php new file mode 100644 index 00000000..a0a99cc2 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/WebPTest.php @@ -0,0 +1,145 @@ +<?php + +/** + * @covers WebPHandler + */ +class WebPHandlerTest extends MediaWikiTestCase { + public function setUp() { + parent::setUp(); + // Allocated file for testing + $this->tempFileName = tempnam( wfTempDir(), 'WEBP' ); + } + public function tearDown() { + parent::tearDown(); + unlink( $this->tempFileName ); + } + /** + * @dataProvider provideTestExtractMetaData + */ + public function testExtractMetaData( $header, $expectedResult ) { + // Put header into file + file_put_contents( $this->tempFileName, $header ); + + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $this->tempFileName ) ); + } + public function provideTestExtractMetaData() { + // phpcs:disable Generic.Files.LineLength + return [ + // Files from https://developers.google.com/speed/webp/gallery2 + [ "\x52\x49\x46\x46\x90\x68\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x83\x68\x01\x00\x2F\x8F\x01\x4B\x10\x8D\x38\x6C\xDB\x46\x92\xE0\xE0\x82\x7B\x6C", + [ 'compression' => 'lossless', 'width' => 400, 'height' => 301 ] ], + [ "\x52\x49\x46\x46\x64\x5B\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x8F\x01\x00\x2C\x01\x00\x41\x4C\x50\x48\xE5\x0E", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 400, 'height' => 301 ] ], + [ "\x52\x49\x46\x46\xA8\x72\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x9B\x72\x00\x00\x2F\x81\x81\x62\x10\x8D\x40\x8C\x24\x39\x6E\x73\x73\x38\x01\x96", + [ 'compression' => 'lossless', 'width' => 386, 'height' => 395 ] ], + [ "\x52\x49\x46\x46\xE0\x42\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x81\x01\x00\x8A\x01\x00\x41\x4C\x50\x48\x56\x10", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 386, 'height' => 395 ] ], + [ "\x52\x49\x46\x46\x70\x61\x02\x00\x57\x45\x42\x50\x56\x50\x38\x4C\x63\x61\x02\x00\x2F\x1F\xC3\x95\x10\x8D\xC8\x72\xDB\xC8\x92\x24\xD8\x91\xD9\x91", + [ 'compression' => 'lossless', 'width' => 800, 'height' => 600 ] ], + [ "\x52\x49\x46\x46\x1C\x1D\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x1F\x03\x00\x57\x02\x00\x41\x4C\x50\x48\x25\x8B", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 800, 'height' => 600 ] ], + [ "\x52\x49\x46\x46\xFA\xC5\x00\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xEE\xC5\x00\x00\x2F\xA4\x81\x28\x10\x8D\x40\x68\x24\xC9\x91\xA4\xAE\xF3\x97\x75", + [ 'compression' => 'lossless', 'width' => 421, 'height' => 163 ] ], + [ "\x52\x49\x46\x46\xF6\x5D\x00\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\xA4\x01\x00\xA2\x00\x00\x41\x4C\x50\x48\x38\x1A", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 421, 'height' => 163 ] ], + [ "\x52\x49\x46\x46\xC4\x96\x01\x00\x57\x45\x42\x50\x56\x50\x38\x4C\xB8\x96\x01\x00\x2F\x2B\xC1\x4A\x10\x11\x87\x6D\xDB\x48\x12\xFC\x60\xB0\x83\x24", + [ 'compression' => 'lossless', 'width' => 300, 'height' => 300 ] ], + [ "\x52\x49\x46\x46\x0A\x11\x01\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x10\x00\x00\x00\x2B\x01\x00\x2B\x01\x00\x41\x4C\x50\x48\x67\x6E", + [ 'compression' => 'unknown', 'animated' => false, 'transparency' => true, 'width' => 300, 'height' => 300 ] ], + + // Lossy files from https://developers.google.com/speed/webp/gallery1 + [ "\x52\x49\x46\x46\x68\x76\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\x5C\x76\x00\x00\xD2\xBE\x01\x9D\x01\x2A\x26\x02\x70\x01\x3E\xD5\x4E\x97\x43\xA2", + [ 'compression' => 'lossy', 'width' => 550, 'height' => 368 ] ], + [ "\x52\x49\x46\x46\xB0\xEC\x00\x00\x57\x45\x42\x50\x56\x50\x38\x20\xA4\xEC\x00\x00\xB2\x4B\x02\x9D\x01\x2A\x26\x02\x94\x01\x3E\xD1\x50\x96\x46\x26", + [ 'compression' => 'lossy', 'width' => 550, 'height' => 404 ] ], + [ "\x52\x49\x46\x46\x7A\x19\x03\x00\x57\x45\x42\x50\x56\x50\x38\x20\x6E\x19\x03\x00\xB2\xF8\x09\x9D\x01\x2A\x00\x05\xD0\x02\x3E\xAD\x46\x99\x4A\xA5", + [ 'compression' => 'lossy', 'width' => 1280, 'height' => 720 ] ], + [ "\x52\x49\x46\x46\x44\xB3\x02\x00\x57\x45\x42\x50\x56\x50\x38\x20\x38\xB3\x02\x00\x52\x57\x06\x9D\x01\x2A\x00\x04\x04\x03\x3E\xA5\x44\x96\x49\x26", + [ 'compression' => 'lossy', 'width' => 1024, 'height' => 772 ] ], + [ "\x52\x49\x46\x46\x02\x43\x01\x00\x57\x45\x42\x50\x56\x50\x38\x20\xF6\x42\x01\x00\x12\xC0\x05\x9D\x01\x2A\x00\x04\xF0\x02\x3E\x79\x34\x93\x47\xA4", + [ 'compression' => 'lossy', 'width' => 1024, 'height' => 752 ] ], + + // Animated file from https://groups.google.com/a/chromium.org/d/topic/blink-dev/Y8tRC4mdQz8/discussion + [ "\x52\x49\x46\x46\xD0\x0B\x02\x00\x57\x45\x42\x50\x56\x50\x38\x58\x0A\x00\x00\x00\x12\x00\x00\x00\x3F\x01\x00\x3F\x01\x00\x41\x4E", + [ 'compression' => 'unknown', 'animated' => true, 'transparency' => true, 'width' => 320, 'height' => 320 ] ], + + // Error cases + [ '', false ], + [ ' ', false ], + [ 'RIFF ', false ], + [ 'RIFF1234WEBP ', false ], + [ 'RIFF1234WEBPVP8 ', false ], + [ 'RIFF1234WEBPVP8L ', false ], + ]; + // phpcs:enable + } + + /** + * @dataProvider provideTestWithFileExtractMetaData + */ + public function testWithFileExtractMetaData( $filename, $expectedResult ) { + $this->assertEquals( $expectedResult, WebPHandler::extractMetadata( $filename ) ); + } + public function provideTestWithFileExtractMetaData() { + return [ + [ __DIR__ . '/../../data/media/2_webp_ll.webp', + [ + 'compression' => 'lossless', + 'width' => 386, + 'height' => 395 + ] + ], + [ __DIR__ . '/../../data/media/2_webp_a.webp', + [ + 'compression' => 'lossy', + 'animated' => false, + 'transparency' => true, + 'width' => 386, + 'height' => 395 + ] + ], + ]; + } + + /** + * @dataProvider provideTestGetImageSize + */ + public function testGetImageSize( $path, $expectedResult ) { + $handler = new WebPHandler(); + $this->assertEquals( $expectedResult, $handler->getImageSize( null, $path ) ); + } + public function provideTestGetImageSize() { + return [ + // Public domain files from https://developers.google.com/speed/webp/gallery2 + [ __DIR__ . '/../../data/media/2_webp_a.webp', [ 386, 395 ] ], + [ __DIR__ . '/../../data/media/2_webp_ll.webp', [ 386, 395 ] ], + [ __DIR__ . '/../../data/media/webp_animated.webp', [ 300, 225 ] ], + + // Error cases + [ __FILE__, false ], + ]; + } + + /** + * Tests the WebP MIME detection. This should really be a separate test, but sticking it + * here for now. + * + * @dataProvider provideTestGetMimeType + */ + public function testGuessMimeType( $path ) { + $mime = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer(); + $this->assertEquals( 'image/webp', $mime->guessMimeType( $path, false ) ); + } + public function provideTestGetMimeType() { + return [ + // Public domain files from https://developers.google.com/speed/webp/gallery2 + [ __DIR__ . '/../../data/media/2_webp_a.webp' ], + [ __DIR__ . '/../../data/media/2_webp_ll.webp' ], + [ __DIR__ . '/../../data/media/webp_animated.webp' ], + ]; + } +} + +/* Python code to extract a header and convert to PHP format: + * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) ) + */ diff --git a/www/wiki/tests/phpunit/includes/media/XCFTest.php b/www/wiki/tests/phpunit/includes/media/XCFTest.php new file mode 100644 index 00000000..b75335d6 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/media/XCFTest.php @@ -0,0 +1,83 @@ +<?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 [ + [ '80x60-2layers.xcf', 80, 60 ], + [ '80x60-RGB.xcf', 80, 60 ], + [ '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 [ + [ '', XCFHandler::METADATA_BAD ], + [ serialize( [ 'error' => true ] ), XCFHandler::METADATA_GOOD ], + [ false, XCFHandler::METADATA_BAD ], + [ serialize( [ '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 [ + [ '80x60-2layers.xcf', + 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' + ], + [ '80x60-RGB.xcf', + 'a:1:{s:9:"colorType";s:16:"truecolour-alpha";}' + ], + [ '80x60-Greyscale.xcf', + 'a:1:{s:9:"colorType";s:15:"greyscale-alpha";}' + ], + ]; + } +} |