From 06cc6cca1ab8fdec6b1a9ba1fc37cb6d81498e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Tisza?= Date: Thu, 6 Feb 2014 23:18:47 +0000 Subject: [PATCH] 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 --- MultimediaViewer.php | 26 +++ MultimediaViewerHooks.php | 1 + resources/mmv/mmv.ThumbnailWidthCalculator.js | 173 ++++++++++++++++++ resources/mmv/mmv.js | 151 +++------------ resources/mmv/mmv.lightboxinterface.js | 20 ++ .../mmv/model/mmv.model.ThumbnailWidth.js | 68 +++++++ .../mmv.ThumbnailWidthCalculator.test.js | 142 ++++++++++++++ tests/qunit/mmv.model.test.js | 17 ++ tests/qunit/mmv.test.js | 77 +------- 9 files changed, 478 insertions(+), 197 deletions(-) create mode 100644 resources/mmv/mmv.ThumbnailWidthCalculator.js create mode 100644 resources/mmv/model/mmv.model.ThumbnailWidth.js create mode 100644 tests/qunit/mmv.ThumbnailWidthCalculator.test.js 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(); } );