diff --git a/MultimediaViewer.php b/MultimediaViewer.php
index 99277f878..4b973ae52 100644
--- a/MultimediaViewer.php
+++ b/MultimediaViewer.php
@@ -144,6 +144,16 @@ call_user_func( function() {
),
), $moduleInfo( 'mmv/model' ) );
+ $wgResourceModules['mmv.model.Thumbnail'] = array_merge( array(
+ 'scripts' => array(
+ 'mmv.model.Thumbnail.js',
+ ),
+
+ 'dependencies' => array(
+ 'mmv.model',
+ ),
+ ), $moduleInfo( 'mmv/model' ) );
+
$wgResourceModules['mmv.provider'] = array_merge( array(
'scripts' => array(
'mmv.provider.Api.js',
@@ -247,6 +257,7 @@ call_user_func( function() {
'mmv.model.FileUsage',
'mmv.model.Image',
'mmv.model.Repo',
+ 'mmv.model.Thumbnail',
'mmv.provider',
'mediawiki.language',
'mmv.multilightbox',
diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js
index 7e1017b9f..ecf47322e 100755
--- a/resources/mmv/mmv.js
+++ b/resources/mmv/mmv.js
@@ -273,28 +273,50 @@
* Gets the API arguments for various calls to the API to find sized thumbnails.
* @param {mw.LightboxInterface} ui
* @returns {Object}
- * @returns {number} return.requested The width that should be requested from the API
- * @returns {number} return.target The ideal width we would like to have - should be the width of the image element later.
+ * @returns {number} return.real The width that should be requested from the API
+ * @returns {number} return.css The ideal width we would like to have - should be the width of the image element later.
*/
MMVP.getImageSizeApiArgs = function ( ui ) {
- var requestedWidth, calculatedMaxWidth,
- thumb = ui.currentImage.thumbnail,
- targetWidth = ui.$imageWrapper.width(),
- targetHeight = ui.$imageWrapper.height();
+ var thumb = ui.currentImage.thumbnail;
- if ( ( targetWidth / targetHeight ) > ( thumb.width / thumb.height ) ) {
- // Need to find width corresponding to highest height we can have.
- calculatedMaxWidth = ( thumb.width / thumb.height ) * targetHeight;
- requestedWidth = this.findNextHighestImageSize( calculatedMaxWidth );
+ return this.getThumbnailWidth( ui.$imageWrapper.width(), ui.$imageWrapper.height(),
+ thumb.width, thumb.height );
+ };
+
+ /**
+ * Finds the largest width for an image so that it will still fit into a given bounding box,
+ * based on the size of a sample (some smaller version of the same image, like the thumbnail
+ * shown in the article) which is used to calculate the ratio.
+ *
+ * Returns two values, a CSS width which is the size in pixels that should be used so the image
+ * fits exactly into the bounding box, and a real width which should be the size of the
+ * downloaded image in pixels. The two will be different for two reasons:
+ * - images are bucketed for more efficient caching, so the real width will always be one of
+ * the numbers in this.imageWidthBuckets
+ * - for devices with high pixel density (multiple actual pixels per CSS pixel) we want to use a
+ * larger image so that there will be roughly one image pixel per physical display pixel
+ *
+ * @param {number} boundingWidth width of the bounding box
+ * @param {number} boundingHeight height of the bounding box
+ * @param {number} sampleWidth width of the sample image
+ * @param {number} sampleHeight height of the sample image
+ * @return {{css: number, real: number}} 'css' field will contain the width of the
+ * thumbnail in CSS pixels, 'real' the actual image size that should be requested.
+ */
+ MMVP.getThumbnailWidth = function( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
+ var cssWidth, bucketedWidth;
+ if ( ( boundingWidth / boundingHeight ) > ( sampleWidth / sampleHeight ) ) {
+ // we are limited by height; we need to calculate the max width that fits
+ cssWidth = ( sampleWidth / sampleHeight ) * boundingHeight;
} else {
- // Simple case, ratio tells us we're limited by width
- requestedWidth = this.findNextHighestImageSize( targetWidth );
+ // simple case, ratio tells us we're limited by width
+ cssWidth = boundingWidth;
}
+ bucketedWidth = this.findNextHighestImageSize( cssWidth );
return {
- // Factor in pixel ratio so we get as many pixels as the device supports, see b/60388
- requested: requestedWidth * $.devicePixelRatio(),
- target: calculatedMaxWidth || targetWidth
+ css: cssWidth,
+ real: bucketedWidth * $.devicePixelRatio()
};
};
@@ -339,11 +361,13 @@
*/
MMVP.resize = function ( ui ) {
var viewer = this,
- fileTitle = this.currentImageFileTitle;
+ fileTitle = this.currentImageFileTitle,
+ imageWidths;
if ( fileTitle ) {
- this.fetchImageInfo( fileTitle ).done( function ( imageData, repoInfo, targetWidth, requestedWidth ) {
- viewer.loadResizedImage( ui, imageData, targetWidth, requestedWidth );
+ imageWidths = this.getImageSizeApiArgs( ui );
+ this.fetchImageInfoWithThumbnail( fileTitle, imageWidths.real ).then( function( imageInfo ) {
+ viewer.loadResizedImage( ui, imageInfo, imageWidths.css, imageWidths.real );
} );
}
@@ -715,17 +739,15 @@
*/
MMVP.fetchImageInfo = function ( fileTitle ) {
var widths = this.getImageSizeApiArgs( this.ui ),
- targetWidth = widths.target,
- requestedWidth = widths.requested;
+ targetWidth = widths.css,
+ requestedWidth = widths.real;
return $.when(
this.fileRepoInfoProvider.get(),
this.imageInfoProvider.get( fileTitle ),
this.thumbnailInfoProvider.get( fileTitle, requestedWidth )
- ).then( function( fileRepoInfoHash, imageInfo, thumbnailData ) {
- var thumbnailUrl = thumbnailData[0],
- thumbnailWidth = thumbnailData[1];
- imageInfo.addThumbUrl( thumbnailWidth, thumbnailUrl );
+ ).then( function( fileRepoInfoHash, imageInfo, thumbnail ) {
+ imageInfo.addThumbUrl( thumbnail.width, thumbnail.url );
return $.Deferred().resolve( imageInfo, fileRepoInfoHash, targetWidth, requestedWidth );
} );
};
diff --git a/resources/mmv/model/mmv.model.Thumbnail.js b/resources/mmv/model/mmv.model.Thumbnail.js
new file mode 100644
index 000000000..b1c8b49e6
--- /dev/null
+++ b/resources/mmv/model/mmv.model.Thumbnail.js
@@ -0,0 +1,47 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see .
+ */
+
+( function ( mw ) {
+ /**
+ * @class mw.mmv.model.Thumbnail
+ * Represents information about an image thumbnail
+ * @constructor
+ * @param {string} url URL to the thumbnail
+ * @param {number} width Width in pixels
+ * @param {number} height Height in pixels
+ */
+ function Thumbnail(
+ url,
+ width,
+ height
+ ) {
+ if ( !url || !width || !height ) {
+ throw 'All parameters are required and cannot be empty or zero';
+ }
+
+ /** @property {string} url The URL to the thumbnail */
+ this.url = url;
+
+ /** @property {number} width The width of the thumbnail in pixels */
+ this.width = width;
+
+ /** @property {number} height The height of the thumbnail in pixels */
+ this.height = height;
+ }
+
+ mw.mmv.model.Thumbnail = Thumbnail;
+}( mediaWiki ) );
diff --git a/resources/mmv/provider/mmv.provider.ThumbnailInfo.js b/resources/mmv/provider/mmv.provider.ThumbnailInfo.js
index 60afbfdeb..6ba5278cd 100644
--- a/resources/mmv/provider/mmv.provider.ThumbnailInfo.js
+++ b/resources/mmv/provider/mmv.provider.ThumbnailInfo.js
@@ -32,16 +32,19 @@
/**
* @method
- * Runs an API GET request to get the thumbnail info.
+ * Runs an API GET request to get the thumbnail info for the specified size.
+ * The thumbnail always has the same aspect ratio as the full image.
+ * One of width or height can be null; if both are set, the API will return the largest
+ * thumbnail which fits into a width x height bounding box (or the full-sized image - whichever
+ * is smaller).
* @param {mw.Title} file
- * @param {number} width thumbnail width
- * @return {jQuery.Promise.} a promise which resolves to the thumbnail URL and
- * the actual width of the thumbnail (which might be smaller than the requested width,
- * in case the size we requested was larger than the full image size).
+ * @param {number} width thumbnail width in pixels
+ * @param {number} height thumbnail height in pixels
+ * @return {jQuery.Promise.}
*/
- ThumbnailInfo.prototype.get = function( file, width ) {
+ ThumbnailInfo.prototype.get = function( file, width, height ) {
var provider = this,
- cacheKey = file.getPrefixedDb() + '|' + width;
+ cacheKey = file.getPrefixedDb() + '|' + ( width || '' ) + '|' + ( height || '' );
if ( !this.cache[cacheKey] ) {
this.cache[cacheKey] = this.api.get( {
@@ -49,13 +52,25 @@
prop: 'imageinfo',
titles: file.getPrefixedDb(),
iiprop: 'url',
- iiurlwidth: width,
+ iiurlwidth: width, // mw.Api will omit null/undefined parameters
+ iiurlheight: height,
format: 'json'
} ).then( function( data ) {
return provider.getQueryPage( file, data );
} ).then( function( page ) {
if ( page.imageinfo && page.imageinfo[0] ) {
- return $.Deferred().resolve( page.imageinfo[0].thumburl, page.imageinfo[0].thumbwidth );
+ var imageInfo = page.imageinfo[0];
+ if ( imageInfo.thumburl && imageInfo.thumbwidth && imageInfo.thumbheight ) {
+ return $.Deferred().resolve(
+ new mw.mmv.model.Thumbnail(
+ imageInfo.thumburl,
+ imageInfo.thumbwidth,
+ imageInfo.thumbheight
+ )
+ );
+ } else {
+ return $.Deferred().reject( 'error in provider, thumb info not found' );
+ }
} else if ( page.missing === '' && page.imagerepository === '' ) {
return $.Deferred().reject( 'file does not exist: ' + file.getPrefixedDb() );
} else {
diff --git a/tests/qunit/mmv.model.test.js b/tests/qunit/mmv.model.test.js
index aac1854d5..7379b4154 100644
--- a/tests/qunit/mmv.model.test.js
+++ b/tests/qunit/mmv.model.test.js
@@ -112,4 +112,21 @@
assert.strictEqual( dbRepo.getArticlePath(), 'http://example.org/wiki/$1', 'DB article path is set correctly' );
assert.strictEqual( apiRepo.getArticlePath(), 'http://example.net/wiki/$1', 'API article path is set correctly' );
} );
+
+ QUnit.test( 'Thumbnail constructor sanity check', 4, function ( assert ) {
+ var width = 23,
+ height = 42,
+ url = 'http://example.com/foo.jpg',
+ thumbnail = new mw.mmv.model.Thumbnail( url, width, height );
+
+ assert.strictEqual( thumbnail.url, url, 'Url is set correctly' );
+ assert.strictEqual( thumbnail.width, width, 'Width is set correctly' );
+ assert.strictEqual( thumbnail.height, height, 'Height is set correctly' );
+
+ try {
+ thumbnail = new mw.mmv.model.Thumbnail( url, width );
+ } catch (e) {
+ assert.ok( e, 'Exception is thrown when parameters are missing');
+ }
+ } );
}( mediaWiki ) );
diff --git a/tests/qunit/mmv.test.js b/tests/qunit/mmv.test.js
index 2514ae988..2d08b8aca 100644
--- a/tests/qunit/mmv.test.js
+++ b/tests/qunit/mmv.test.js
@@ -514,8 +514,8 @@
widths = viewer.getImageSizeApiArgs( ui );
- assert.strictEqual( widths.target, 150/100*200, 'Correct target width was computed.' );
- assert.strictEqual( widths.requested, 320 * $.devicePixelRatio(), 'Correct requested width was computed.' );
+ assert.strictEqual( widths.css, 150/100*200, 'Correct CSS width was computed.' );
+ assert.strictEqual( widths.real, 320 * $.devicePixelRatio(), 'Correct real width was computed.' );
// Fake viewport dimensions, width/height == 1.0, we are limited by width
ui.$imageWrapper.height( 600 );
@@ -523,8 +523,8 @@
widths = viewer.getImageSizeApiArgs( ui );
- assert.strictEqual( widths.target, 600, 'Correct target width was computed.' );
- assert.strictEqual( widths.requested, 640 * $.devicePixelRatio(), 'Correct requested width was computed.' );
+ assert.strictEqual( widths.css, 600, 'Correct CSS width was computed.' );
+ assert.strictEqual( widths.real, 640 * $.devicePixelRatio(), 'Correct real width was computed.' );
ui.unattach();
diff --git a/tests/qunit/provider/mmv.provider.ThumbnailInfo.test.js b/tests/qunit/provider/mmv.provider.ThumbnailInfo.test.js
index 27b4145c3..1961ae276 100644
--- a/tests/qunit/provider/mmv.provider.ThumbnailInfo.test.js
+++ b/tests/qunit/provider/mmv.provider.ThumbnailInfo.test.js
@@ -25,7 +25,7 @@
assert.ok( thumbnailInfoProvider );
} );
- QUnit.asyncTest( 'ThumbnailInfo get test', 5, function ( assert ) {
+ QUnit.asyncTest( 'ThumbnailInfo get test', 7, function ( assert ) {
var apiCallCount = 0,
api = { get: function() {
apiCallCount++;
@@ -54,11 +54,12 @@
file = new mw.Title( 'File:Stuff.jpg' ),
thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
- thumbnailInfoProvider.get( file, 100 ).then( function( thumnailUrl, thumbnailWidth ) {
- assert.strictEqual( thumnailUrl,
+ thumbnailInfoProvider.get( file, 100 ).then( function( thumbnail ) {
+ assert.strictEqual( thumbnail.url,
'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg',
'URL is set correctly' );
- assert.strictEqual( thumbnailWidth, 95, 'actual width is set correctly' );
+ assert.strictEqual( thumbnail.width, 95, 'actual width is set correctly' );
+ assert.strictEqual( thumbnail.height, 200, 'actual height is set correctly' );
} ).then( function() {
assert.strictEqual( apiCallCount, 1 );
// call the data provider a second time to check caching
@@ -69,6 +70,10 @@
return thumbnailInfoProvider.get( file, 110 );
} ).then( function() {
assert.strictEqual( apiCallCount, 2 );
+ // call it again, with a height specified, to check caching
+ return thumbnailInfoProvider.get( file, 110, 100 );
+ } ).then( function() {
+ assert.strictEqual( apiCallCount, 3 );
QUnit.start();
} );
} );
@@ -130,4 +135,28 @@
QUnit.start();
} );
} );
+
+ QUnit.asyncTest( 'ThumbnailInfo fail test 3', 1, function ( assert ) {
+ var api = { get: function() {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg',
+ imageinfo: [
+ {}
+ ]
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ thumbnailInfoProvider.get( file, 100 ).fail( function() {
+ assert.ok( true, 'promise rejected when thumbnail info is missing' );
+ QUnit.start();
+ } );
+ } );
}( mediaWiki, jQuery ) );