mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer
synced 2024-11-24 08:13:38 +00:00
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:
parent
e74fc33e89
commit
06cc6cca1a
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
173
resources/mmv/mmv.ThumbnailWidthCalculator.js
Normal file
173
resources/mmv/mmv.ThumbnailWidthCalculator.js
Normal 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 ) );
|
|
@ -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' );
|
||||
|
||||
|
|
|
@ -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 ) );
|
||||
|
|
68
resources/mmv/model/mmv.model.ThumbnailWidth.js
Normal file
68
resources/mmv/model/mmv.model.ThumbnailWidth.js
Normal 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 ) );
|
142
tests/qunit/mmv.ThumbnailWidthCalculator.test.js
Normal file
142
tests/qunit/mmv.ThumbnailWidthCalculator.test.js
Normal 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 ) );
|
|
@ -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 ) );
|
||||
|
|
|
@ -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();
|
||||
} );
|
||||
|
||||
|
|
Loading…
Reference in a new issue