Add Thumbnail model

Also refactor size calculation a bit - I found target/requested
harder to remember.

Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/155
Change-Id: I4781cdd7004e9a8e36875c152e1d3a335a55b7d7
This commit is contained in:
Gergő Tisza 2014-02-05 01:06:10 +00:00
parent d1e966e903
commit e33f2d263c
7 changed files with 182 additions and 41 deletions

View file

@ -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',

View file

@ -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 );
} );
};

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
( 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 ) );

View file

@ -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.<string, number>} 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.<mw.mmv.model.Thumbnail>}
*/
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 {

View file

@ -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 ) );

View file

@ -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();

View file

@ -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 ) );