Refactor thumbnail size calculation

* moves generic logic into ThumbnailSizeCalculator class
* moves UI-specific logic into interface class
* fixes bug where non-bucketed sizes were served on devices with
  non-standard pixel density
* fixes bug where bucketed size was compared to css size instead of screen size
  for resizing

Change-Id: I8ba3380b74fcc8fb0a6ecc3f3140627411851ad0
Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/196
This commit is contained in:
Gergő Tisza 2014-02-06 23:18:47 +00:00
parent e74fc33e89
commit 06cc6cca1a
9 changed files with 478 additions and 197 deletions

View file

@ -92,6 +92,21 @@ call_user_func( function() {
'mmv.ui.description',
'mmv.ui.fileUsage',
'mmv.ui.metadataPanel',
'mmv.ThumbnailWidthCalculator',
),
), $moduleInfo( 'mmv' ) );
$wgResourceModules['mmv.ThumbnailWidthCalculator'] = array_merge( array(
'scripts' => array(
'mmv.ThumbnailWidthCalculator.js',
),
'dependencies' => array(
'mediawiki',
'jquery',
'jquery.hidpi',
'mmv.base',
'mmv.model.ThumbnailWidth',
),
), $moduleInfo( 'mmv' ) );
@ -158,6 +173,16 @@ call_user_func( function() {
),
), $moduleInfo( 'mmv/model' ) );
$wgResourceModules['mmv.model.ThumbnailWidth'] = array_merge( array(
'scripts' => array(
'mmv.model.ThumbnailWidth.js',
),
'dependencies' => array(
'mmv.model',
),
), $moduleInfo( 'mmv/model' ) );
$wgResourceModules['mmv.provider'] = array_merge( array(
'scripts' => array(
'mmv.provider.Api.js',
@ -342,6 +367,7 @@ call_user_func( function() {
'mediawiki.language',
'mmv.multilightbox',
'mmv.performance',
'mmv.ThumbnailWidthCalculator',
),
'messages' => array(

View file

@ -123,6 +123,7 @@ class MultimediaViewerHooks {
'tests/qunit/mmv.testhelpers.js',
'tests/qunit/mmv.test.js',
'tests/qunit/mmv.model.test.js',
'tests/qunit/mmv.ThumbnailWidthCalculator.test.js',
'tests/qunit/provider/mmv.provider.Api.test.js',
'tests/qunit/mmv.performance.test.js',
'tests/qunit/provider/mmv.provider.ImageUsage.test.js',

View file

@ -0,0 +1,173 @@
/*
* 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, $ ) {
var TWCP;
/**
* @class mw.mmv.ThumbnailWidthCalculator
*
* A helper class for bucketing image sizes.
* Bucketing helps to avoid cache fragmentation and thus speed up image loading:
* instead of generating potentially hundreds of different thumbnail sizes, we restrict
* ourselves to a short list of acceptable thumbnail widths, and only ever load thumbnails
* of that size. Final size adjustment is done in a thumbnail.
*
* See also the [Standardized thumbnail sizes RFC][1]
*
* [1]: https://www.mediawiki.org/wiki/Talk:Requests_for_comment/Standardized_thumbnails_sizes
*
* @constructor
* @param {Object} [options]
* @param {number[]} [options.widthBuckets] see {@link mw.mmv.ThumbnailWidthCalculator#widthBuckets}
* @param {number} [options.devicePixelRatio] see {@link mw.mmv.ThumbnailWidthCalculator#devicePixelRatio};
* will be autodetected if omitted
*/
function ThumbnailWidthCalculator( options ) {
options = $.extend( {}, this.defaultOptions, options );
if ( !options.widthBuckets.length ) {
throw 'No buckets!';
}
/**
* List of thumbnail width bucket sizes, in pixels.
* @property {number[]}
*/
this.widthBuckets = options.widthBuckets;
this.widthBuckets.sort( function( a, b ) { return a - b; } );
/**
* Screen pixel count per CSS pixel.
* @property {number}
*/
this.devicePixelRatio = options.devicePixelRatio;
}
TWCP = ThumbnailWidthCalculator.prototype;
/**
* The default list of image widths
* @static
* @property {Object}
*/
TWCP.defaultOptions = {
// default image widths
widthBuckets: [
320,
640,
800,
1024,
1280,
1920,
2560,
2880
],
// screen pixel per CSS pixel
devicePixelRatio: $.devicePixelRatio()
};
/**
* @method
* Finds the smallest bucket which is large enough to hold the target size
* (i. e. the smallest bucket whose size is equal to or greater than the target).
* If none of the buckets are large enough, returns the largest bucket.
* @param {number} target
* @return {number}
*/
TWCP.findNextBucket = function ( target ) {
var i, bucket,
buckets = this.widthBuckets;
for ( i = 0; i < buckets.length; i++ ) {
bucket = buckets[i];
if ( bucket >= target ) {
return bucket;
}
}
// If we failed to find a high enough size...good luck
return bucket;
};
/**
* @method
* @protected
* 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.
*
* This is for internal use, you should probably use calculateWidths() instead.
*
* @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 {number} the largest width so that the scaled version of the sample image fits
* into the bounding box (either horizontal or vertical edges touch on both sides).
*/
TWCP.calculateFittingWidth = function( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
if ( ( boundingWidth / boundingHeight ) > ( sampleWidth / sampleHeight ) ) {
// we are limited by height; we need to calculate the max width that fits
return ( sampleWidth / sampleHeight ) * boundingHeight;
} else {
// simple case, ratio tells us we're limited by width
return boundingWidth;
}
};
/**
* @method
* 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.widthBuckets. The resulting thumbnail will be slightly larger than
* the bounding box so that it takes roughly the same amount of bandwidth and
* looks decent when resized by the browser.
* - 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, in CSS pixels
* @param {number} boundingHeight height of the bounding box, in CSS pixels
* @param {number} sampleWidth width of the sample image (in whatever - only used for aspect ratio)
* @param {number} sampleHeight height of the sample image (in whatever - only used for aspect ratio)
* @return {mw.mmv.model.ThumbnailWidth}
*/
TWCP.calculateWidths = function( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
var cssWidth,
screenPixelWidth,
bucketedWidth;
cssWidth = this.calculateFittingWidth( boundingWidth, boundingHeight, sampleWidth, sampleHeight );
screenPixelWidth = cssWidth * this.devicePixelRatio;
bucketedWidth = this.findNextBucket( screenPixelWidth );
return new mw.mmv.model.ThumbnailWidth( cssWidth, screenPixelWidth, bucketedWidth );
};
mw.mmv.ThumbnailWidthCalculator = ThumbnailWidthCalculator;
}( mediaWiki, jQuery ) );

View file

@ -57,28 +57,18 @@
urls = [],
viewer = this;
/**
* @property {number[]}
* @private
* List of acceptable image sizes...used to bucket
*/
this.imageWidthBuckets = [
320,
640,
800,
1024,
1280,
1920,
2560,
2880
];
/**
* @property {mw.Api}
* @private
*/
this.api = new mw.Api();
/**
* @type {mw.mmv.ThumbnailWidthCalculator}
* @private
*/
this.thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
/**
* @property {mw.mmv.provider.ImageInfo}
* @private
@ -231,80 +221,6 @@
return thisImage;
};
/**
* Finds the next highest image size given a target size.
* Searches the bucketed sizes configured in the class.
* @param {number} target
* @return {number}
*/
MMVP.findNextHighestImageSize = function ( target ) {
var i, bucket,
buckets = this.imageWidthBuckets,
len = buckets.length;
for ( i = 0; i < len; i++ ) {
bucket = buckets[i];
if ( bucket >= target ) {
return bucket;
}
}
// If we failed to find a high enough size...good luck
return bucket;
};
/**
* Gets the API arguments for various calls to the API to find sized thumbnails.
* @param {mw.LightboxInterface} ui
* @returns {Object}
* @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 thumb = ui.currentImage.thumbnail;
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
cssWidth = boundingWidth;
}
bucketedWidth = this.findNextHighestImageSize( cssWidth );
return {
css: cssWidth,
real: bucketedWidth * $.devicePixelRatio()
};
};
/**
* Handles clicks on legit image links.
*
@ -350,32 +266,15 @@
imageWidths;
if ( fileTitle ) {
imageWidths = this.getImageSizeApiArgs( ui );
imageWidths = ui.getImageSizeApiArgs();
this.fetchImageInfoWithThumbnail( fileTitle, imageWidths.real ).then( function( imageInfo ) {
viewer.loadResizedImage( ui, imageInfo, imageWidths.css, imageWidths.real );
viewer.loadAndSetImage( ui, imageInfo, imageWidths );
} );
}
this.updateControls();
};
/**
* Replaces the resized image in the viewer providing we actually got some data.
*
* @protected
*
* @param {mw.LightboxInterface} ui lightbox that got resized
* @param {mw.mmv.model.Image} imageData information regarding the new resized image
* @param {number} targetWidth
* @param {number} requestedWidth
*/
MMVP.loadResizedImage = function ( ui, imageData, targetWidth, requestedWidth ) {
// Replace image only if data was returned.
if ( imageData ) {
this.loadAndSetImage( ui, imageData, targetWidth, requestedWidth );
}
};
MMVP.updateControls = function () {
var numImages = this.lightbox.images ? this.lightbox.images.length : 0,
showNextButton = this.lightbox.currentIndex < (numImages - 1),
@ -410,28 +309,32 @@
* and collects profiling information.
*
* @param {mw.LightboxInterface} ui image container
* @param {mw.mmv.model.Image} imageData image information
* @param {number} targetWidth
* @param {number} requestedWidth
* @param {mw.mmv.model.Image} imageInfo image information
* @param {mw.mmv.model.ThumbnailWidth} imageWidths
*/
MMVP.loadAndSetImage = function ( ui, imageData, targetWidth, requestedWidth ) {
MMVP.loadAndSetImage = function ( ui, imageInfo, imageWidths ) {
var maybeThumb,
viewer = this,
image = new Image(),
imageWidth,
src;
// Use cached image if we have it.
maybeThumb = imageData.getThumbUrl( requestedWidth );
src = maybeThumb || imageData.url;
maybeThumb = imageInfo.getThumbUrl( imageWidths.real );
if ( maybeThumb ) {
src = maybeThumb;
imageWidth = imageWidths.real;
} else {
src = imageInfo.url;
imageWidth = imageInfo.width;
}
this.performance.record( 'image', src ).then( function() {
image.src = src;
if ( maybeThumb && requestedWidth > targetWidth ||
!maybeThumb && imageData.width > targetWidth ) {
// Image bigger than the current area, resize before loading
image.width = targetWidth;
// we downscale larger images but do not scale up smaller ones, that would look ugly
if ( imageWidth > imageWidths.screen ) {
image.width = imageWidths.css;
}
ui.replaceImageWith( image );
@ -446,7 +349,7 @@
* @param {string} initialSrc The string to set the src attribute to at first.
*/
MMVP.loadImage = function ( image, initialSrc ) {
var imageWidth,
var imageWidths,
viewer = this;
this.lightbox.currentIndex = image.index;
@ -470,9 +373,9 @@
$( document.body ).addClass( 'mw-mlb-lightbox-open' );
imageWidth = this.getImageSizeApiArgs( this.ui );
imageWidths = this.ui.getImageSizeApiArgs();
this.fetchImageInfoRepoInfoAndFileUsageInfo(
image.filePageTitle, imageWidth.real
image.filePageTitle, imageWidths.real
).then( function ( imageInfo, repoInfoHash, thumbnail, localUsage, globalUsage ) {
var repoInfo = repoInfoHash[imageInfo.repo];
@ -481,7 +384,7 @@
// We need to wait until the animation is finished before we listen to scroll
.then( function() { viewer.startListeningToScroll(); } );
viewer.loadAndSetImage( viewer.lightbox.iface, imageInfo, imageWidth.css, imageWidth.real );
viewer.loadAndSetImage( viewer.lightbox.iface, imageInfo, imageWidths );
viewer.lightbox.iface.$imageDiv.removeClass( 'empty' );

View file

@ -31,6 +31,12 @@
this.eventsRegistered = {};
/**
* Copy of {@link mw.MultimediaViewer#thumbnailWidthCalculator}
* @property {mw.mmv.ThumbnailWidthCalculator}
*/
this.thumbnailWidthCalculator = viewer.thumbnailWidthCalculator;
this.initializeInterface();
}
@ -362,5 +368,19 @@
this.revealButtonsAndFadeIfNeeded();
};
/**
* Gets the API arguments for various calls to the API to find sized thumbnails.
* @returns {Object}
* @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.
*/
LIP.getImageSizeApiArgs = function () {
var thumb = this.currentImage.thumbnail;
return this.thumbnailWidthCalculator.calculateWidths(
this.$imageWrapper.width(), this.$imageWrapper.height(), thumb.width, thumb.height );
};
mw.LightboxInterface = LightboxInterface;
}( mediaWiki, jQuery, OO, window.LightboxInterface ) );

View file

@ -0,0 +1,68 @@
/*
* 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/>.
*/
/**
* @class mw.mmv.model.ThumbnailWidth
* Represents image width information.
*
* To utilize caching as much as possible, we use images which are displayed at a slightly
* different size than their screen size. The ThumbnailWidth model stores the various types of
* sizes and helps avoiding accidental incompatible assignments. (Think of it as a slightly
* overcomplicated Hungarian notation :)
*
* @constructor
* @param {number} css width in CSS pixels
* @param {number} screen width in screen pixels
* @param {number} real width in real pixels
*/
function ThumbnailWidth( css, screen, real ) {
if ( !css || !screen || !real ) {
throw 'All parameters are required and cannot be empty or zero';
}
/**
* Width of the thumbnail on the screen, in CSS pixels. This is the number which can be plugged
* into UI code like $element.width(x).
* @property {number}
*/
this.css = css;
/**
* Width of the thumbnail on the screen, in device pixels. On most devices this is the same as
* the CSS width, but devices with high pixel density displays have multiple screen pixels
* in a CSS pixel.
* This value is mostly used internally; for most purposes you will need one of the others.
* @property {number}
*/
this.screen = screen;
/**
* "Real" width of the thumbnail. This is the number you need to use in API requests when
* obtaining the thumbnail URL. This is usually larger than the screen width, since
* downscaling images via CSS looks OK but upscaling them looks ugly. However, for images
* where the full size itself is very small, this can be smaller than the screen width, since
* we cannot create a thumbnail which is larger than the original image. (In such cases the
* image is just positioned to the center of the intended area and the space around it is
* left empty.)
* @property {number}
*/
this.real = real;
}
( function ( mw ) {
mw.mmv.model.ThumbnailWidth = ThumbnailWidth;
}( mediaWiki ) );

View file

@ -0,0 +1,142 @@
( function ( mw ) {
QUnit.module( 'mmv.ThumbnailWidthCalculator', QUnit.newMwEnvironment() );
QUnit.test( 'ThumbnailWidthCalculator constructor sanity check', 4, function ( assert ) {
var badWidthBuckets = [],
goodWidthBuckets = [1],
thumbnailWidthCalculator;
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
assert.ok( thumbnailWidthCalculator, 'constructor with no argument works');
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {} );
assert.ok( thumbnailWidthCalculator, 'constructor with empty option argument works');
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: goodWidthBuckets
} );
assert.ok( thumbnailWidthCalculator, 'constructor with non-default buckets works');
try {
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: badWidthBuckets
} );
} catch (e) {
assert.ok( e, 'constructor with empty bucket list throws exception');
}
} );
QUnit.test( 'findNextBucket() test', 4, function ( assert ) {
var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: [ 100, 200 ]
} );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100,
'return first bucket for value smaller than all buckets' );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200,
'return last bucket for value larger than all buckets' );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200,
'return next bucket for value between two buckets' );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 100 ), 100,
'return bucket for value equal to that bucket' );
} );
// Old tests for the default bucket sizes. Preserved because why not.
QUnit.test( 'We get sane image sizes when we ask for them', 5, function ( assert ) {
var twc = new mw.mmv.ThumbnailWidthCalculator();
assert.strictEqual( twc.findNextBucket( 200 ), 320, 'Low target size gives us lowest possible size bucket' );
assert.strictEqual( twc.findNextBucket( 320 ), 320, 'Asking for a bucket size gives us exactly that bucket size' );
assert.strictEqual( twc.findNextBucket( 320.00001 ), 640, 'Asking for greater than an image bucket definitely gives us the next size up' );
assert.strictEqual( twc.findNextBucket( 2000 ), 2560, 'The image bucketing also works on big screens' );
assert.strictEqual( twc.findNextBucket( 3000 ), 2880, 'The image bucketing also works on REALLY big screens' );
} );
QUnit.test( 'findNextBucket() test with unordered bucket list', 3, function ( assert ) {
var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: [ 200, 100 ]
} );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100,
'return first bucket for value smaller than all buckets' );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200,
'return last bucket for value larger than all buckets' );
assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200,
'return next bucket for value between two buckets' );
} );
QUnit.test( 'calculateFittingWidth() test', 3, function ( assert ) {
var boundingWidth = 100,
boundingHeight = 200,
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { widthBuckets: [ 1 ] } );
// 50x10 image in 100x200 box - need to scale up 2x
assert.strictEqual(
thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 50, 10 ),
100, 'fit calculation correct when limited by width' );
// 10x100 image in 100x200 box - need to scale up 2x
assert.strictEqual(
thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 100 ),
20, 'fit calculation correct when limited by height' );
// 10x20 image in 100x200 box - need to scale up 10x
assert.strictEqual(
thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 20 ),
100, 'fit calculation correct when same aspect ratio' );
} );
QUnit.test( 'calculateWidths() test', 6, function ( assert ) {
var boundingWidth = 100,
boundingHeight = 200,
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ]
} ),
widths;
// 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 128x25.6
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 );
assert.strictEqual( widths.css, 100, 'css width is correct when limited by width' );
assert.strictEqual( widths.real, 128, 'real width is correct when limited by width' );
// 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 32x320
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 );
assert.strictEqual( widths.css, 20, 'css width is correct when limited by height' );
assert.strictEqual( widths.real, 32, 'real width is correct when limited by height' );
// 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 128x256
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 );
assert.strictEqual( widths.css, 100, 'css width is correct when same aspect ratio' );
assert.strictEqual( widths.real, 128, 'real width is correct when same aspect ratio' );
} );
QUnit.test( 'calculateWidths() test with non-standard device pixel ratio', 6, function ( assert ) {
var boundingWidth = 100,
boundingHeight = 200,
thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ],
devicePixelRatio: 2
} ),
widths;
// 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 256x51.2
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 );
assert.strictEqual( widths.css, 100, 'css width is correct when limited by width' );
assert.strictEqual( widths.real, 256, 'real width is correct when limited by width' );
// 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 64x640
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 );
assert.strictEqual( widths.css, 20, 'css width is correct when limited by height' );
assert.strictEqual( widths.real, 64, 'real width is correct when limited by height' );
// 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 256x512
widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 );
assert.strictEqual( widths.css, 100, 'css width is correct when same aspect ratio' );
assert.strictEqual( widths.real, 256, 'real width is correct when same aspect ratio' );
} );
}( mediaWiki ) );

View file

@ -129,4 +129,21 @@
assert.ok( e, 'Exception is thrown when parameters are missing');
}
} );
QUnit.test( 'ThumbnailWidth constructor sanity check', 4, function ( assert ) {
var cssWidth = 23,
screenWidth = 42,
realWidth = 123,
thumbnailWidth = new mw.mmv.model.ThumbnailWidth( cssWidth, screenWidth, realWidth );
assert.strictEqual( thumbnailWidth.css, cssWidth, 'Url is set correctly' );
assert.strictEqual( thumbnailWidth.screen, screenWidth, 'Width is set correctly' );
assert.strictEqual( thumbnailWidth.real, realWidth, 'Height is set correctly' );
try {
thumbnailWidth = new mw.mmv.model.ThumbnailWidth( cssWidth, screenWidth );
} catch (e) {
assert.ok( e, 'Exception is thrown when parameters are missing');
}
} );
}( mediaWiki ) );

View file

@ -125,20 +125,6 @@
mw.mmvTestHelpers.resetViewer();
} );
QUnit.test( 'Do not load the resized image if no data returning from the api', 1, function ( assert ) {
var ui,
data,
viewer = new mw.MultimediaViewer();
// Calling loadResizedImage() with empty/undefined data should not fail.
viewer.loadResizedImage( ui, data );
assert.ok( true, 'Resized image is not replaced since we have not data.' );
// Clean up the viewer, to avoid seeing it catch events when running other tests
mw.mmvTestHelpers.resetViewer();
} );
QUnit.test( 'Ensure that the click callback is getting the appropriate initial value for image loading', 1, function ( assert ) {
var imgSrc = '300px-valid.jpg',
div = createGallery( imgSrc ),
@ -205,19 +191,6 @@
mw.mmvTestHelpers.resetViewer();
} );
QUnit.test( 'We get sane image sizes when we ask for them', 5, function ( assert ) {
var viewer = new mw.MultimediaViewer();
assert.strictEqual( viewer.findNextHighestImageSize( 200 ), 320, 'Low target size gives us lowest possible size bucket' );
assert.strictEqual( viewer.findNextHighestImageSize( 320 ), 320, 'Asking for a bucket size gives us exactly that bucket size' );
assert.strictEqual( viewer.findNextHighestImageSize( 320.00001 ), 640, 'Asking for greater than an image bucket definitely gives us the next size up' );
assert.strictEqual( viewer.findNextHighestImageSize( 2000 ), 2560, 'The image bucketing also works on big screens' );
assert.strictEqual( viewer.findNextHighestImageSize( 3000 ), 2880, 'The image bucketing also works on REALLY big screens' );
// Clean up the viewer, to avoid seeing it catch events when running other tests
mw.mmvTestHelpers.resetViewer();
} );
QUnit.test( 'Metadata div is only animated once', 4, function ( assert ) {
var viewer = new mw.MultimediaViewer(),
backupAnimation = $.fn.animate,
@ -262,50 +235,10 @@
mw.mmvTestHelpers.resetViewer();
} );
QUnit.test( 'getImageSizeApiArgs(): Limited by height and limited by width', 4, function ( assert ) {
var widths,
viewer = new mw.MultimediaViewer(),
ui = new mw.LightboxInterface( viewer );
// Fake thumbnail, width/height == 1.5
ui.currentImage = {
thumbnail: {
height: 100,
width: 150
}
};
ui.attach( '#qunit-fixture' );
// Fake viewport dimensions, width/height == 2.0, we are limited by height
ui.$imageWrapper.height( 200 );
ui.$imageWrapper.width( 400 );
widths = viewer.getImageSizeApiArgs( ui );
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 );
ui.$imageWrapper.width( 600 );
widths = viewer.getImageSizeApiArgs( ui );
assert.strictEqual( widths.css, 600, 'Correct CSS width was computed.' );
assert.strictEqual( widths.real, 640 * $.devicePixelRatio(), 'Correct real width was computed.' );
ui.unattach();
// Clean up the viewer, to avoid seeing it catch events when running other tests
mw.mmvTestHelpers.resetViewer();
} );
QUnit.asyncTest( 'loadAndSetImage(): Basic load', 3, function ( assert ) {
var targetWidth,
requestedWidth,
var widths = new mw.mmv.model.ThumbnailWidth( 8, 8, 640 ), // Current area < imageData.width
viewer = new mw.MultimediaViewer(),
ui = new mw.LightboxInterface(),
ui = new mw.LightboxInterface( viewer ),
size = 120,
width = 10,
height = 11,
@ -321,7 +254,7 @@
ui.replaceImageWith = function ( image ) {
assert.strictEqual( image.src, imageUrl, 'Image to replace has correct "src" attribute.' );
assert.strictEqual( image.width, targetWidth, 'Image to replace has correct "width" attribute.' );
assert.strictEqual( image.width, widths.css, 'Image to replace has correct "width" attribute.' );
};
viewer.updateControls = function () {
assert.ok( true, 'Controls updated.' );
@ -329,9 +262,7 @@
};
// Test case when image loaded is bigger than current area
targetWidth = 8; // Current area < imageData.width
requestedWidth = 640;
viewer.loadAndSetImage( ui, imageData, targetWidth, requestedWidth );
viewer.loadAndSetImage( ui, imageData, widths );
mw.mmvTestHelpers.resetViewer();
} );