diff --git a/MultimediaViewer.php b/MultimediaViewer.php
index 40ee73428..86129436b 100644
--- a/MultimediaViewer.php
+++ b/MultimediaViewer.php
@@ -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(
diff --git a/MultimediaViewerHooks.php b/MultimediaViewerHooks.php
index 8bbf71496..688c3ca26 100644
--- a/MultimediaViewerHooks.php
+++ b/MultimediaViewerHooks.php
@@ -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',
diff --git a/resources/mmv/mmv.ThumbnailWidthCalculator.js b/resources/mmv/mmv.ThumbnailWidthCalculator.js
new file mode 100644
index 000000000..760c2cf0d
--- /dev/null
+++ b/resources/mmv/mmv.ThumbnailWidthCalculator.js
@@ -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 .
+ */
+
+( 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 ) );
diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js
index 95ca84219..8065ffaca 100755
--- a/resources/mmv/mmv.js
+++ b/resources/mmv/mmv.js
@@ -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' );
diff --git a/resources/mmv/mmv.lightboxinterface.js b/resources/mmv/mmv.lightboxinterface.js
index c67bcc9fd..116db9ab8 100644
--- a/resources/mmv/mmv.lightboxinterface.js
+++ b/resources/mmv/mmv.lightboxinterface.js
@@ -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 ) );
diff --git a/resources/mmv/model/mmv.model.ThumbnailWidth.js b/resources/mmv/model/mmv.model.ThumbnailWidth.js
new file mode 100644
index 000000000..3a0fd41c4
--- /dev/null
+++ b/resources/mmv/model/mmv.model.ThumbnailWidth.js
@@ -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 .
+ */
+
+/**
+ * @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 ) );
diff --git a/tests/qunit/mmv.ThumbnailWidthCalculator.test.js b/tests/qunit/mmv.ThumbnailWidthCalculator.test.js
new file mode 100644
index 000000000..80d681805
--- /dev/null
+++ b/tests/qunit/mmv.ThumbnailWidthCalculator.test.js
@@ -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 ) );
diff --git a/tests/qunit/mmv.model.test.js b/tests/qunit/mmv.model.test.js
index 7379b4154..70025c898 100644
--- a/tests/qunit/mmv.model.test.js
+++ b/tests/qunit/mmv.model.test.js
@@ -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 ) );
diff --git a/tests/qunit/mmv.test.js b/tests/qunit/mmv.test.js
index 54f449983..4f9010722 100644
--- a/tests/qunit/mmv.test.js
+++ b/tests/qunit/mmv.test.js
@@ -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();
} );