mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer
synced 2024-11-12 09:27:36 +00:00
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:
parent
d1e966e903
commit
e33f2d263c
|
@ -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',
|
||||
|
|
|
@ -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 );
|
||||
} );
|
||||
};
|
||||
|
|
47
resources/mmv/model/mmv.model.Thumbnail.js
Normal file
47
resources/mmv/model/mmv.model.Thumbnail.js
Normal 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 ) );
|
|
@ -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 {
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
Loading…
Reference in a new issue