Index: branches/wmf/1.18wmf1/tests/phpunit/includes/media/ExifRotationTest.php |
— | — | @@ -9,41 +9,74 @@ |
10 | 10 | parent::setUp(); |
11 | 11 | $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; |
12 | 12 | $this->handler = new BitmapHandler(); |
| 13 | + $this->repo = new FSRepo(array( |
| 14 | + 'name' => 'temp', |
| 15 | + 'directory' => wfTempDir() . '/exif-test-' . time(), |
| 16 | + 'url' => 'http://localhost/thumbtest' |
| 17 | + )); |
| 18 | + if ( !wfDl( 'exif' ) ) { |
| 19 | + $this->markTestSkipped( "This test needs the exif extension." ); |
| 20 | + } |
| 21 | + global $wgShowEXIF; |
| 22 | + $this->show = $wgShowEXIF; |
| 23 | + $wgShowEXIF = true; |
13 | 24 | } |
| 25 | + public function tearDown() { |
| 26 | + global $wgShowEXIF; |
| 27 | + $wgShowEXIF = $this->show; |
| 28 | + } |
14 | 29 | |
15 | 30 | /** |
16 | 31 | * |
17 | 32 | * @dataProvider providerFiles |
18 | 33 | */ |
19 | 34 | function testMetadata( $name, $type, $info ) { |
20 | | - # Force client side resizing |
21 | | - $params = array( 'width' => 10000, 'height' => 10000 ); |
22 | 35 | $file = UnregisteredLocalFile::newFromPath( $this->filePath . $name, $type ); |
23 | | - |
24 | | - # Normalize parameters |
25 | | - $this->assertTrue( $this->handler->normaliseParams( $file, $params ) ); |
26 | | - $rotation = $this->handler->getRotation( $file ); |
27 | | - |
28 | | - # Check if pre-rotation dimensions are still good |
29 | | - list( $width, $height ) = $this->handler->extractPreRotationDimensions( $params, $rotation ); |
30 | | - $this->assertEquals( $file->getWidth(), $width, |
31 | | - "$name: pre-rotation width check, $rotation:$width" ); |
32 | | - $this->assertEquals( $file->getHeight(), $height, |
33 | | - "$name: pre-rotation height check, $rotation" ); |
34 | | - |
35 | | - # Any further test require a scaler that can rotate |
36 | | - if ( !BitmapHandler::canRotate() ) { |
37 | | - $this->markTestIncomplete( 'Scaler does not support rotation' ); |
38 | | - return; |
| 36 | + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); |
| 37 | + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * |
| 42 | + * @dataProvider providerFiles |
| 43 | + */ |
| 44 | + function testRotationRendering( $name, $type, $info, $thumbs ) { |
| 45 | + foreach( $thumbs as $size => $out ) { |
| 46 | + if( preg_match('/^(\d+)px$/', $size, $matches ) ) { |
| 47 | + $params = array( |
| 48 | + 'width' => $matches[1], |
| 49 | + ); |
| 50 | + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { |
| 51 | + $params = array( |
| 52 | + 'width' => $matches[1], |
| 53 | + 'height' => $matches[2] |
| 54 | + ); |
| 55 | + } else { |
| 56 | + throw new MWException('bogus test data format ' . $size); |
| 57 | + } |
| 58 | + |
| 59 | + $file = $this->localFile( $name, $type ); |
| 60 | + $thumb = $file->transform( $params, File::RENDER_NOW ); |
| 61 | + |
| 62 | + $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" ); |
| 63 | + $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" ); |
| 64 | + |
| 65 | + $gis = getimagesize( $thumb->getPath() ); |
| 66 | + if ($out[0] > $info['width']) { |
| 67 | + // Physical image won't be scaled bigger than the original. |
| 68 | + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size"); |
| 69 | + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size"); |
| 70 | + } else { |
| 71 | + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size"); |
| 72 | + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size"); |
| 73 | + } |
39 | 74 | } |
40 | | - |
41 | | - # Check post-rotation width |
42 | | - $this->assertEquals( $params['physicalWidth'], $info['width'], |
43 | | - "$name: post-rotation width check" ); |
44 | | - $this->assertEquals( $params['physicalHeight'], $info['height'], |
45 | | - "$name: post-rotation height check" ); |
46 | 75 | } |
47 | 76 | |
| 77 | + private function localFile( $name, $type ) { |
| 78 | + return new UnregisteredLocalFile( false, $this->repo, $this->filePath . $name, $type ); |
| 79 | + } |
| 80 | + |
48 | 81 | function providerFiles() { |
49 | 82 | return array( |
50 | 83 | array( |
— | — | @@ -52,6 +85,12 @@ |
53 | 86 | array( |
54 | 87 | 'width' => 1024, |
55 | 88 | 'height' => 768, |
| 89 | + ), |
| 90 | + array( |
| 91 | + '800x600px' => array( 800, 600 ), |
| 92 | + '9999x800px' => array( 1067, 800 ), |
| 93 | + '800px' => array( 800, 600 ), |
| 94 | + '600px' => array( 600, 450 ), |
56 | 95 | ) |
57 | 96 | ), |
58 | 97 | array( |
— | — | @@ -60,6 +99,12 @@ |
61 | 100 | array( |
62 | 101 | 'width' => 768, // as rotated |
63 | 102 | 'height' => 1024, // as rotated |
| 103 | + ), |
| 104 | + array( |
| 105 | + '800x600px' => array( 450, 600 ), |
| 106 | + '9999x800px' => array( 600, 800 ), |
| 107 | + '800px' => array( 800, 1067 ), |
| 108 | + '600px' => array( 600, 800 ), |
64 | 109 | ) |
65 | 110 | ) |
66 | 111 | ); |
— | — | @@ -100,22 +145,5 @@ |
101 | 146 | ), |
102 | 147 | ); |
103 | 148 | } |
104 | | - |
105 | | - function testWidthFlipping() { |
106 | | - $file = UnregisteredLocalFile::newFromPath( $this->filePath . 'portrait-rotated.jpg', 'image/jpeg' ); |
107 | | - $params = array( 'width' => '50' ); |
108 | | - $this->assertTrue( $this->handler->normaliseParams( $file, $params ) ); |
109 | | - |
110 | | - $this->assertEquals( 50, $params['height'] ); |
111 | | - $this->assertEquals( round( (768/1024)*50 ), $params['width'], '', 0.1 ); |
112 | | - } |
113 | | - function testWidthNotFlipping() { |
114 | | - $file = UnregisteredLocalFile::newFromPath( $this->filePath . 'landscape-plain.jpg', 'image/jpeg' ); |
115 | | - $params = array( 'width' => '50' ); |
116 | | - $this->assertTrue( $this->handler->normaliseParams( $file, $params ) ); |
117 | | - |
118 | | - $this->assertEquals( 50, $params['width'] ); |
119 | | - $this->assertEquals( round( (768/1024)*50 ), $params['height'], '', 0.1 ); |
120 | | - } |
121 | 149 | } |
122 | 150 | |
Index: branches/wmf/1.18wmf1/includes/media/Bitmap.php |
— | — | @@ -42,20 +42,6 @@ |
43 | 43 | return true; |
44 | 44 | } |
45 | 45 | } |
46 | | - |
47 | | - |
48 | | - if ( self::canRotate() ) { |
49 | | - $rotation = $this->getRotation( $image ); |
50 | | - if ( $rotation == 90 || $rotation == 270 ) { |
51 | | - wfDebug( __METHOD__ . ": Swapping width and height because the file will be rotated $rotation degrees\n" ); |
52 | | - |
53 | | - list( $params['width'], $params['height'] ) = |
54 | | - array( $params['height'], $params['width'] ); |
55 | | - list( $params['physicalWidth'], $params['physicalHeight'] ) = |
56 | | - array( $params['physicalHeight'], $params['physicalWidth'] ); |
57 | | - } |
58 | | - } |
59 | | - |
60 | 46 | |
61 | 47 | # Don't thumbnail an image so big that it will fill hard drives and send servers into swap |
62 | 48 | # JPEG has the handy property of allowing thumbnailing without full decompression, so we make |
— | — | @@ -73,6 +59,11 @@ |
74 | 60 | /** |
75 | 61 | * Extracts the width/height if the image will be scaled before rotating |
76 | 62 | * |
| 63 | + * This will match the physical size/aspect ratio of the original image |
| 64 | + * prior to application of the rotation -- so for a portrait image that's |
| 65 | + * stored as raw landscape with 90-degress rotation, the resulting size |
| 66 | + * will be wider than it is tall. |
| 67 | + * |
77 | 68 | * @param $params array Parameters as returned by normaliseParams |
78 | 69 | * @param $rotation int The rotation angle that will be applied |
79 | 70 | * @return array ($width, $height) array |
— | — | @@ -249,6 +240,8 @@ |
250 | 241 | * @param $image File File associated with this thumbnail |
251 | 242 | * @param $params array Array with scaler params |
252 | 243 | * @return ThumbnailImage |
| 244 | + * |
| 245 | + * @fixme no rotation support |
253 | 246 | */ |
254 | 247 | protected function getClientScalingThumbnailImage( $image, $params ) { |
255 | 248 | return new ThumbnailImage( $image, $image->getURL(), |
— | — | @@ -685,33 +678,21 @@ |
686 | 679 | } |
687 | 680 | |
688 | 681 | /** |
689 | | - * Try to read out the orientation of the file and return the angle that |
690 | | - * the file needs to be rotated to be viewed |
| 682 | + * On supporting image formats, try to read out the low-level orientation |
| 683 | + * of the file and return the angle that the file needs to be rotated to |
| 684 | + * be viewed. |
691 | 685 | * |
| 686 | + * This information is only useful when manipulating the original file; |
| 687 | + * the width and height we normally work with is logical, and will match |
| 688 | + * any produced output views. |
| 689 | + * |
| 690 | + * The base BitmapHandler doesn't understand any metadata formats, so this |
| 691 | + * is left up to child classes to implement. |
| 692 | + * |
692 | 693 | * @param $file File |
693 | 694 | * @return int 0, 90, 180 or 270 |
694 | 695 | */ |
695 | 696 | public function getRotation( $file ) { |
696 | | - $data = $file->getMetadata(); |
697 | | - if ( !$data ) { |
698 | | - return 0; |
699 | | - } |
700 | | - wfSuppressWarnings(); |
701 | | - $data = unserialize( $data ); |
702 | | - wfRestoreWarnings(); |
703 | | - if ( isset( $data['Orientation'] ) ) { |
704 | | - # See http://sylvana.net/jpegcrop/exif_orientation.html |
705 | | - switch ( $data['Orientation'] ) { |
706 | | - case 8: |
707 | | - return 90; |
708 | | - case 3: |
709 | | - return 180; |
710 | | - case 6: |
711 | | - return 270; |
712 | | - default: |
713 | | - return 0; |
714 | | - } |
715 | | - } |
716 | 697 | return 0; |
717 | 698 | } |
718 | 699 | |
Index: branches/wmf/1.18wmf1/includes/media/ExifBitmap.php |
— | — | @@ -124,5 +124,77 @@ |
125 | 125 | function getMetadataType( $image ) { |
126 | 126 | return 'exif'; |
127 | 127 | } |
| 128 | + |
| 129 | + /** |
| 130 | + * Wrapper for base classes ImageHandler::getImageSize() that checks for |
| 131 | + * rotation reported from metadata and swaps the sizes to match. |
| 132 | + * |
| 133 | + * @param File $image |
| 134 | + * @param string $path |
| 135 | + * @return array |
| 136 | + */ |
| 137 | + function getImageSize( $image, $path ) { |
| 138 | + $gis = parent::getImageSize( $image, $path ); |
| 139 | + |
| 140 | + // Don't just call $image->getMetadata(); File::getPropsFromPath() calls us with a bogus object. |
| 141 | + // This may mean we read EXIF data twice on initial upload. |
| 142 | + $meta = $this->getMetadata( $image, $path ); |
| 143 | + $rotation = $this->getRotationForExif( $meta ); |
| 144 | + |
| 145 | + if ($rotation == 90 || $rotation == 270) { |
| 146 | + $width = $gis[0]; |
| 147 | + $gis[0] = $gis[1]; |
| 148 | + $gis[1] = $width; |
| 149 | + } |
| 150 | + return $gis; |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * On supporting image formats, try to read out the low-level orientation |
| 155 | + * of the file and return the angle that the file needs to be rotated to |
| 156 | + * be viewed. |
| 157 | + * |
| 158 | + * This information is only useful when manipulating the original file; |
| 159 | + * the width and height we normally work with is logical, and will match |
| 160 | + * any produced output views. |
| 161 | + * |
| 162 | + * @param $file File |
| 163 | + * @return int 0, 90, 180 or 270 |
| 164 | + */ |
| 165 | + public function getRotation( $file ) { |
| 166 | + $data = $file->getMetadata(); |
| 167 | + return $this->getRotationForExif( $data ); |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Given a chunk of serialized Exif metadata, return the orientation as |
| 172 | + * degrees of rotation. |
| 173 | + * |
| 174 | + * @param string $data |
| 175 | + * @return int 0, 90, 180 or 270 |
| 176 | + * @fixme orientation can include flipping as well; see if this is an issue! |
| 177 | + */ |
| 178 | + protected function getRotationForExif( $data ) { |
| 179 | + if ( !$data ) { |
| 180 | + return 0; |
| 181 | + } |
| 182 | + wfSuppressWarnings(); |
| 183 | + $data = unserialize( $data ); |
| 184 | + wfRestoreWarnings(); |
| 185 | + if ( isset( $data['Orientation'] ) ) { |
| 186 | + # See http://sylvana.net/jpegcrop/exif_orientation.html |
| 187 | + switch ( $data['Orientation'] ) { |
| 188 | + case 8: |
| 189 | + return 90; |
| 190 | + case 3: |
| 191 | + return 180; |
| 192 | + case 6: |
| 193 | + return 270; |
| 194 | + default: |
| 195 | + return 0; |
| 196 | + } |
| 197 | + } |
| 198 | + return 0; |
| 199 | + } |
128 | 200 | } |
129 | 201 | |
Index: branches/wmf/1.18wmf1/resources/mediawiki.special/mediawiki.special.upload.js |
— | — | @@ -83,7 +83,7 @@ |
84 | 84 | } |
85 | 85 | |
86 | 86 | img.onload = function() { |
87 | | - var width, height, x, y, dx, dy; |
| 87 | + var width, height, x, y, dx, dy, logicalWidth, logicalHeight; |
88 | 88 | // Fit the image within the previewSizexpreviewSize box |
89 | 89 | if ( img.width > img.height ) { |
90 | 90 | width = previewSize; |
— | — | @@ -103,19 +103,27 @@ |
104 | 104 | case 0: |
105 | 105 | x = dx; |
106 | 106 | y = dy; |
| 107 | + logicalWidth = img.width; |
| 108 | + logicalHeight = img.height; |
107 | 109 | break; |
108 | 110 | case 90: |
109 | 111 | |
110 | 112 | x = dx; |
111 | 113 | y = dy - previewSize; |
| 114 | + logicalWidth = img.height; |
| 115 | + logicalHeight = img.width; |
112 | 116 | break; |
113 | 117 | case 180: |
114 | 118 | x = dx - previewSize; |
115 | 119 | y = dy - previewSize; |
| 120 | + logicalWidth = img.width; |
| 121 | + logicalHeight = img.height; |
116 | 122 | break; |
117 | 123 | case 270: |
118 | 124 | x = dx - previewSize; |
119 | 125 | y = dy; |
| 126 | + logicalWidth = img.height; |
| 127 | + logicalHeight = img.width; |
120 | 128 | break; |
121 | 129 | } |
122 | 130 | |
— | — | @@ -124,7 +132,7 @@ |
125 | 133 | ctx.drawImage( img, x, y, width, height ); |
126 | 134 | |
127 | 135 | // Image size |
128 | | - var info = mw.msg( 'widthheight', img.width, img.height ) + |
| 136 | + var info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) + |
129 | 137 | ', ' + prettySize( file.size ); |
130 | 138 | $( '#mw-upload-thumbnail .fileinfo' ).text( info ); |
131 | 139 | }; |